diff --git a/lib/galaxy/actions/library.py b/lib/galaxy/actions/library.py index 4f5f902d8b6..8fec9fb7494 100644 --- a/lib/galaxy/actions/library.py +++ b/lib/galaxy/actions/library.py @@ -5,6 +5,10 @@ Contains library functions import json import logging import os.path +from typing import ( + Any, + Protocol, +) from markupsafe import escape @@ -12,10 +16,15 @@ from galaxy import ( exceptions, util, ) +from galaxy.managers.collections import DatasetCollectionManager from galaxy.managers.collections_util import ( api_payload_to_create_params, dictify_dataset_collection_instance, ) +from galaxy.managers.context import ( + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.model import ( HistoryDatasetAssociation, Library, @@ -32,7 +41,7 @@ from galaxy.util.path import ( log = logging.getLogger(__name__) -def validate_server_directory_upload(trans, server_dir): +def validate_server_directory_upload(trans: ProvidesUserContext, server_dir): if server_dir in [None, "None", ""]: raise exceptions.RequestParameterInvalidException("Invalid or unspecified server_dir parameter") @@ -81,7 +90,7 @@ def validate_server_directory_upload(trans, server_dir): return full_dir, import_dir_desc -def validate_path_upload(trans): +def validate_path_upload(trans: ProvidesUserContext): if not trans.app.config.allow_library_path_paste: raise exceptions.ConfigDoesNotAllowException( '"allow_path_paste" is not set to True in the Galaxy configuration file' @@ -93,12 +102,24 @@ def validate_path_upload(trans): ) +class CreatesLibraryCollections(Protocol): + """What LibraryActions._create_collection needs from its host class. + + Hosts inject collection_manager; check_user_can_add_to_library_item comes + from UsesLibraryMixinItems, which they all mix in as well. + """ + + collection_manager: DatasetCollectionManager + + def check_user_can_add_to_library_item(self, trans: ProvidesUserContext, item, check_accessible: bool = True): ... + + class LibraryActions: """ Mixin for controllers that provide library functionality. """ - def _upload_dataset(self, trans, folder_id: int, payload): + def _upload_dataset(self, trans: ProvidesHistoryContext, folder_id: int, payload): # Set up the traditional tool state/params cntrller = "api" tool_id = "upload1" @@ -106,6 +127,8 @@ class LibraryActions: datatypes_registry=trans.app.datatypes_registry, ext=payload.file_type ) tool = trans.app.toolbox.get_tool(tool_id) + if tool is None: + raise exceptions.ToolMissingException(f"Tool '{tool_id}' is missing from the toolbox") state = tool.new_state(trans) populate_state(trans, tool.inputs, payload.model_dump(), state.inputs) tool_params = state.inputs @@ -160,7 +183,9 @@ class LibraryActions: raise exceptions.RequestParameterInvalidException("Upload failed") return output - def _get_server_dir_uploaded_datasets(self, trans, payload, full_dir, import_dir_desc, library_bunch): + def _get_server_dir_uploaded_datasets( + self, trans: ProvidesHistoryContext, payload, full_dir, import_dir_desc, library_bunch + ): files = self._get_server_dir_files(payload, full_dir, import_dir_desc) uploaded_datasets = [] for file in files: @@ -206,9 +231,11 @@ class LibraryActions: raise exceptions.ObjectAttributeMissingException(f"The directory '{full_dir}' contains no valid files") return files - def _get_path_paste_uploaded_datasets(self, trans, params, library_bunch, response_code, message): + def _get_path_paste_uploaded_datasets( + self, trans: ProvidesHistoryContext, params, library_bunch, response_code, message + ): preserve_dirs = util.string_as_bool(params.get("preserve_dirs", False)) - uploaded_datasets = [] + uploaded_datasets: list = [] files_and_folders, _response_code, _message = self._get_path_files_and_folders(params, preserve_dirs) if _response_code: return (uploaded_datasets, _response_code, _message) @@ -264,12 +291,14 @@ class LibraryActions: return None, response_code, message return None - def _make_library_uploaded_dataset(self, trans, params, name, path, type, library_bunch, in_folder=None): + def _make_library_uploaded_dataset( + self, trans: ProvidesHistoryContext, params, name, path, type, library_bunch, in_folder=None + ): link_data_only = params.get("link_data_only", "copy_files") uuid_str = params.get("uuid", None) file_type = params.get("file_type", None) library_bunch.replace_dataset = None # not valid for these types of upload - uploaded_dataset = util.bunch.Bunch() + uploaded_dataset: Any = util.bunch.Bunch() new_name = name # Remove compressed file extensions, if any, but only if # we're copying files into Galaxy's file space. @@ -300,7 +329,7 @@ class LibraryActions: trans.sa_session.commit() return uploaded_dataset - def _upload_library_dataset(self, trans, payload): + def _upload_library_dataset(self, trans: ProvidesHistoryContext, payload): is_admin = trans.user_is_admin current_user_roles = trans.get_current_user_roles() folder = trans.sa_session.get(LibraryFolder, payload.folder_id) @@ -320,7 +349,7 @@ class LibraryActions: created_outputs_dict = self._upload_dataset(trans, folder.id, payload) return created_outputs_dict - def _create_folder(self, trans, payload): + def _create_folder(self, trans: ProvidesUserContext, payload): is_admin = trans.user_is_admin current_user_roles = trans.get_current_user_roles() parent_folder = trans.sa_session.get(LibraryFolder, payload.folder_id) @@ -343,7 +372,7 @@ class LibraryActions: new_folder_dict = dict(created=new_folder) return new_folder_dict - def _create_collection(self, trans, payload, parent): + def _create_collection(self: CreatesLibraryCollections, trans: ProvidesUserContext, payload, parent): # Not delegating to library_common, so need to check access to parent folder here. self.check_user_can_add_to_library_item(trans, parent, check_accessible=True) create_params = api_payload_to_create_params(payload.model_dump()) @@ -356,13 +385,14 @@ class LibraryActions: ) return [dataset_collection] - def _check_access(self, trans, is_admin, item, current_user_roles): + def _check_access(self, trans: ProvidesUserContext, is_admin, item, current_user_roles): if isinstance(item, HistoryDatasetAssociation): # Make sure the user has the DATASET_ACCESS permission on the history_dataset_association. if not item: message = f"Invalid history dataset ({escape(str(item))}) specified." raise exceptions.ObjectNotFound(message) - elif ( + assert item.dataset is not None + if ( not trans.app.security_agent.can_access_dataset(current_user_roles, item.dataset) and item.user == trans.user ): @@ -385,7 +415,7 @@ class LibraryActions: message = f"You do not have permission to access the {escape(item_type)} with id ({str(item.id)})." raise exceptions.ItemAccessibilityException(message) - def _check_add(self, trans, is_admin, item, current_user_roles): + def _check_add(self, trans: ProvidesUserContext, is_admin, item, current_user_roles): # Deny access if the user is not an admin and does not have the LIBRARY_ADD permission. if not (is_admin or trans.app.security_agent.can_add_library_item(current_user_roles, item)): message = f"You are not authorized to add an item to ({escape(item.name)})." diff --git a/lib/galaxy/agents/base.py b/lib/galaxy/agents/base.py index ed32b599e7d..60f2f2c3394 100644 --- a/lib/galaxy/agents/base.py +++ b/lib/galaxy/agents/base.py @@ -29,13 +29,13 @@ from typing import ( import yaml from galaxy.exceptions import ConfigurationError -from galaxy.managers.context import ProvidesUserContext from galaxy.model import User from galaxy.schema.agents import ( ActionSuggestion, ActionType, ConfidenceLevel, ) +from galaxy.work.context import SessionRequestContext if TYPE_CHECKING: from galaxy.config import GalaxyAppConfiguration @@ -353,7 +353,7 @@ class AgentRunState: class GalaxyAgentDependencies: """Dependencies passed to Galaxy agents via dependency injection.""" - trans: ProvidesUserContext + trans: SessionRequestContext user: User config: "GalaxyAppConfiguration" # Callable to get agent instances, avoids circular import in base.py diff --git a/lib/galaxy/agents/operations.py b/lib/galaxy/agents/operations.py index 666f4bd8861..8a84254ddae 100644 --- a/lib/galaxy/agents/operations.py +++ b/lib/galaxy/agents/operations.py @@ -13,7 +13,6 @@ from typing import ( from sqlalchemy import select from galaxy.agents import iwc -from galaxy.managers.context import ProvidesUserContext from galaxy.managers.hdas import HDAManager from galaxy.managers.tools import DynamicToolManager from galaxy.model import UserDynamicToolAssociation @@ -40,6 +39,7 @@ from galaxy.schema.schema import ( from galaxy.schema.workflows import InvokeWorkflowPayload from galaxy.structured_app import MinimalManagerApp from galaxy.tool_util_models.dynamic_tool_models import DynamicUnprivilegedToolCreatePayload +from galaxy.work.context import SessionRequestContext log = logging.getLogger(__name__) @@ -61,7 +61,7 @@ ID_FIELDS = { class AgentOperationsManager: """Shared operations for AI agents, delegating to Galaxy's service layer.""" - def __init__(self, app: MinimalManagerApp, trans: ProvidesUserContext): + def __init__(self, app: MinimalManagerApp, trans: SessionRequestContext): self.app = app self.trans = trans self._tools_service: Any | None = None diff --git a/lib/galaxy/agents/tools.py b/lib/galaxy/agents/tools.py index 2daf5b23f63..64bfa7a9bf0 100644 --- a/lib/galaxy/agents/tools.py +++ b/lib/galaxy/agents/tools.py @@ -246,7 +246,7 @@ class ToolRecommendationAgent(BaseGalaxyAgent): try: panel_view = self.deps.config.default_panel_view or "default" - toolbox_search = self.deps.trans.app.toolbox_search # type: ignore[attr-defined] + toolbox_search = self.deps.trans.app.toolbox_search tool_ids = toolbox_search.search(query, panel_view, self.deps.config) tools = [] diff --git a/lib/galaxy/app/__init__.py b/lib/galaxy/app/__init__.py index fa592f20172..e9d19862fe7 100644 --- a/lib/galaxy/app/__init__.py +++ b/lib/galaxy/app/__init__.py @@ -977,7 +977,9 @@ class UniverseApplication(StructuredApp, GalaxyManagerApplication, InstallationT self.proxy_manager = ProxyManager(self.config) # Must be initialized after job_config. - self.workflow_scheduling_manager = scheduling_manager.WorkflowSchedulingManager(self) + self.workflow_scheduling_manager = self._register_singleton( + scheduling_manager.WorkflowSchedulingManager, scheduling_manager.WorkflowSchedulingManager(self) + ) # Initialize workflow completion monitoring (manager is always available, # but monitor only runs on workflow scheduler processes) diff --git a/lib/galaxy/app_unittest_utils/galaxy_mock.py b/lib/galaxy/app_unittest_utils/galaxy_mock.py index 3a421fb1a0a..a8c41dd4bd8 100644 --- a/lib/galaxy/app_unittest_utils/galaxy_mock.py +++ b/lib/galaxy/app_unittest_utils/galaxy_mock.py @@ -28,6 +28,7 @@ from galaxy.config_watchers import ConfigWatchers from galaxy.job_metrics import JobMetrics from galaxy.jobs.manager import NoopManager from galaxy.managers.collections import DatasetCollectionManager +from galaxy.managers.context import ProvidesAppContext from galaxy.managers.dbkeys import GenomeBuilds from galaxy.managers.hdas import HDAManager from galaxy.managers.histories import HistoryManager @@ -430,7 +431,7 @@ class MockTrans: class MockVisualizationsRegistry: - def get_visualizations(self, trans, target): + def get_visualizations(self, trans: ProvidesAppContext, target): return [] diff --git a/lib/galaxy/auth/__init__.py b/lib/galaxy/auth/__init__.py index 87bc7a43da2..5a2bf1f224c 100644 --- a/lib/galaxy/auth/__init__.py +++ b/lib/galaxy/auth/__init__.py @@ -3,6 +3,7 @@ Contains implementations of the authentication logic. """ import logging +from typing import TYPE_CHECKING from galaxy.auth.util import ( get_authenticators, @@ -11,6 +12,9 @@ from galaxy.auth.util import ( from galaxy.exceptions import Conflict from galaxy.util import string_as_bool +if TYPE_CHECKING: + from galaxy.webapps.base.webapp import GalaxyWebTransaction + log = logging.getLogger(__name__) @@ -41,7 +45,7 @@ class AuthManager: break return message, status - def check_auto_registration(self, trans, login, password, no_password_check=False): + def check_auto_registration(self, trans: "GalaxyWebTransaction", login, password, no_password_check=False): """ Checks the username/email & password using auth providers in order. If a match is found, returns the 'auto-register' option for that provider. diff --git a/lib/galaxy/auth/util.py b/lib/galaxy/auth/util.py index 720e43e1f80..0fe7ddfa4f3 100644 --- a/lib/galaxy/auth/util.py +++ b/lib/galaxy/auth/util.py @@ -1,6 +1,10 @@ import errno import logging from collections import namedtuple +from typing import ( + Any, + TYPE_CHECKING, +) import galaxy.auth.providers from galaxy.exceptions import Conflict @@ -12,6 +16,10 @@ from galaxy.util import ( string_as_bool, ) +if TYPE_CHECKING: + # runtime cycle: galaxy.managers.context -> galaxy.structured_app -> galaxy.auth -> here + from galaxy.managers.context import ProvidesAppContext + log = logging.getLogger(__name__) AUTH_CONF_XML = """ @@ -73,8 +81,8 @@ def get_authenticators(auth_config_file, auth_config_file_set): return authenticators -def parse_auth_results(trans, auth_results, options): - auth_return = {} +def parse_auth_results(trans: "ProvidesAppContext", auth_results, options): + auth_return: dict[str, Any] = {} auth_result, auto_email, auto_username = auth_results[:3] auto_username = str(auto_username).lower() # make username unique diff --git a/lib/galaxy/authnz/__init__.py b/lib/galaxy/authnz/__init__.py index 071b09784bf..d102ed6f6dd 100644 --- a/lib/galaxy/authnz/__init__.py +++ b/lib/galaxy/authnz/__init__.py @@ -9,6 +9,11 @@ Additionally, this package implements functionalist's to request temporary acces credentials for cloud-based resource providers (e.g., Amazon AWS, Microsoft Azure). """ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from galaxy.webapps.base.webapp import GalaxyWebTransaction + class IdentityProvider: """ @@ -40,10 +45,10 @@ class IdentityProvider: """ raise NotImplementedError() - def refresh(self, trans, token): + def refresh(self, trans: "GalaxyWebTransaction", token): raise NotImplementedError() - def authenticate(self, trans, idphint=None): + def authenticate(self, trans: "GalaxyWebTransaction", idphint=None): """Runs for authentication process. Checks the database if a valid identity exists in the database; if yes, then the user is authenticated, if not, it generates a provider-specific @@ -56,7 +61,7 @@ class IdentityProvider: """ raise NotImplementedError() - def callback(self, state_token: str, authz_code: str, trans, login_redirect_url): + def callback(self, state_token: str, authz_code: str, trans: "GalaxyWebTransaction", login_redirect_url): """Handles authentication call-backs from identity providers. This process maps `state-token` to a user. @@ -72,10 +77,12 @@ class IdentityProvider: """ raise NotImplementedError() - def disconnect(self, provider, trans, disconnect_redirect_url=None, email=None, association_id=None): + def disconnect( + self, provider, trans: "GalaxyWebTransaction", disconnect_redirect_url=None, email=None, association_id=None + ): raise NotImplementedError() - def logout(self, trans, post_user_logout_href=None): + def logout(self, trans: "GalaxyWebTransaction", post_user_logout_href=None): """ Return a URL that will log the user out of the IDP. In OIDC this is called the 'end_session_endpoint'. diff --git a/lib/galaxy/authnz/managers.py b/lib/galaxy/authnz/managers.py index ceb9ecc87b3..d177aba4fef 100644 --- a/lib/galaxy/authnz/managers.py +++ b/lib/galaxy/authnz/managers.py @@ -40,7 +40,10 @@ from .psa_authnz import ( ) if TYPE_CHECKING: - from galaxy.managers.context import ProvidesAppContext + from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesUserContext, + ) from galaxy.webapps.base.webapp import GalaxyWebTransaction OIDC_BACKEND_SCHEMA = resource_path(__name__, "xsd/oidc_backends_config.xsd") @@ -300,7 +303,7 @@ class AuthnzManager: return None @staticmethod - def can_user_assume_authn(trans, authn_id): + def can_user_assume_authn(trans: ProvidesUserContext, authn_id): qres = trans.sa_session.query(model.UserAuthnzToken).get(authn_id) if qres is None: msg = f"Authentication record with the given `authn_id` (`{trans.security.encode_id(authn_id)}`) not found." @@ -405,9 +408,13 @@ class AuthnzManager: log.exception(msg) return False, msg, None - def callback(self, provider, state_token, authz_code, trans, login_redirect_url, idphint=None): + def callback( + self, provider, state_token, authz_code, trans: GalaxyWebTransaction, login_redirect_url, idphint=None + ): try: success, message, backend = self._get_authnz_backend(provider, idphint=idphint) + if backend is None: + return False, f"Provider `{provider}` not found", (None, None) if success is False: return False, message, (None, None) return success, message, backend.callback(state_token, authz_code, trans, login_redirect_url) @@ -505,7 +512,7 @@ class AuthnzManager: return user return None - def logout(self, provider, trans, post_user_logout_href=None): + def logout(self, provider, trans: GalaxyWebTransaction, post_user_logout_href=None): """ Log the user out of the identity provider. @@ -525,6 +532,8 @@ class AuthnzManager: return False, f"IDP logout is not enabled for {provider}", None success, message, backend = self._get_authnz_backend(provider) + if backend is None: + return False, f"Provider `{provider}` not found", None if success is False: return False, message, None return True, message, backend.logout(trans, post_user_logout_href) @@ -533,9 +542,11 @@ class AuthnzManager: log.exception(msg) return False, msg, None - def disconnect(self, provider, trans, email=None, disconnect_redirect_url=None, idphint=None): + def disconnect(self, provider, trans: GalaxyWebTransaction, email=None, disconnect_redirect_url=None, idphint=None): try: success, message, backend = self._get_authnz_backend(provider, idphint=idphint) + if backend is None: + return False, f"Provider `{provider}` not found", None if success is False: return False, message, None return backend.disconnect(provider, trans, disconnect_redirect_url, email=email) diff --git a/lib/galaxy/authnz/psa_authnz.py b/lib/galaxy/authnz/psa_authnz.py index 546df52bce4..54d6d4e107b 100644 --- a/lib/galaxy/authnz/psa_authnz.py +++ b/lib/galaxy/authnz/psa_authnz.py @@ -54,6 +54,7 @@ if TYPE_CHECKING: from social_core.strategy import HttpResponseProtocol from galaxy.managers.context import ProvidesAppContext + from galaxy.webapps.base.webapp import GalaxyWebTransaction log = logging.getLogger(__name__) @@ -293,7 +294,7 @@ class PSAAuthnz(IdentityProvider): extra_data["expires"] = int(expires - time.time()) user_authnz_token.set_extra_data(extra_data) - def refresh(self, trans, user_authnz_token): + def refresh(self, trans: "GalaxyWebTransaction", user_authnz_token): if ( not user_authnz_token or not user_authnz_token.extra_data @@ -318,13 +319,13 @@ class PSAAuthnz(IdentityProvider): def _try_to_locate_token_expiration(self, extra_data): return locate_token_expiration(extra_data) - def authenticate(self, trans, idphint=None) -> "HttpResponseProtocol": + def authenticate(self, trans: "GalaxyWebTransaction", idphint=None) -> "HttpResponseProtocol": on_the_fly_config(trans.sa_session) strategy = Strategy(trans.request, trans.session, Storage, self.config) backend = self._load_backend(strategy, self.config["redirect_uri"]) return do_auth(backend) - def callback(self, state_token, authz_code, trans, login_redirect_url): + def callback(self, state_token, authz_code, trans: "GalaxyWebTransaction", login_redirect_url): on_the_fly_config(trans.sa_session) # Always set LOGIN_REDIRECT_URL to the base URL for pipeline steps # We'll adjust the final redirect based on fixed_delegated_auth after do_complete @@ -384,7 +385,9 @@ class PSAAuthnz(IdentityProvider): return redirect_url, user - def disconnect(self, provider, trans, disconnect_redirect_url=None, email=None, association_id=None): + def disconnect( + self, provider, trans: "GalaxyWebTransaction", disconnect_redirect_url=None, email=None, association_id=None + ): on_the_fly_config(trans.sa_session) self.config[setting_name("DISCONNECT_REDIRECT_URL")] = ( disconnect_redirect_url if disconnect_redirect_url is not None else () @@ -397,7 +400,7 @@ class PSAAuthnz(IdentityProvider): return True, "", response_url return response.get("success", False), response.get("message", ""), "" - def logout(self, trans, post_user_logout_href=None): + def logout(self, trans: "GalaxyWebTransaction", post_user_logout_href=None): """ Logout from the identity provider. @@ -846,7 +849,7 @@ def sync_user_profile(strategy=None, details=None, user=None, **kwargs): _send_oidc_profile_update_notification(trans, user, updates) -def _send_oidc_profile_update_notification(trans, user, updates: list[str]) -> None: +def _send_oidc_profile_update_notification(trans: "ProvidesAppContext", user, updates: list[str]) -> None: if not trans.app.notification_manager.notifications_enabled: return try: diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py index 736fcb66e67..d1fe7df167c 100644 --- a/lib/galaxy/datatypes/binary.py +++ b/lib/galaxy/datatypes/binary.py @@ -112,7 +112,9 @@ except ModuleNotFoundError: pass if TYPE_CHECKING: + from galaxy.managers.context import ProvidesUserContext from galaxy.util.compression_utils import FileObjType + from galaxy.webapps.base.webapp import GalaxyWebTransaction log = logging.getLogger(__name__) # pysam 0.16.0.1 emits logs containing the word 'Error', this can confuse the stdout/stderr checkers. @@ -777,7 +779,9 @@ class BamNative(CompressedArchive, _BamOrSam): # Remove temp file and empty temporary directory os.rmdir(tmp_dir) - def get_chunk(self, trans, dataset: HasFileName, offset: int = 0, ck_size: int | None = None) -> str: + def get_chunk( + self, trans: "ProvidesUserContext | None", dataset: HasFileName, offset: int = 0, ck_size: int | None = None + ) -> str: if not offset == -1: try: with pysam.AlignmentFile( @@ -830,7 +834,7 @@ class BamNative(CompressedArchive, _BamOrSam): def display_data( self, - trans, + trans: "GalaxyWebTransaction", dataset: DatasetHasHidProtocol, preview: bool = False, filename: str | None = None, @@ -2537,7 +2541,7 @@ class H5MLM(H5): def display_data( self, - trans, + trans: "GalaxyWebTransaction", dataset: DatasetHasHidProtocol, preview: bool = False, filename: str | None = None, diff --git a/lib/galaxy/datatypes/data.py b/lib/galaxy/datatypes/data.py index 986fee2d120..2b564344d7e 100644 --- a/lib/galaxy/datatypes/data.py +++ b/lib/galaxy/datatypes/data.py @@ -34,6 +34,7 @@ from galaxy.datatypes.protocols import ( HasExt, HasExtraFilesAndMetadata, HasFileName, + HasHid, HasInfo, HasMetadata, HasName, @@ -69,6 +70,11 @@ from . import ( if TYPE_CHECKING: from galaxy.datatypes.display_applications.application import DisplayApplication from galaxy.datatypes.registry import Registry + from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesUserContext, + ) + from galaxy.webapps.base.webapp import GalaxyWebTransaction XSS_VULNERABLE_MIME_TYPES = [ "image/svg+xml", # Unfiltered by Galaxy and may contain JS that would be executed by some browsers. @@ -397,7 +403,7 @@ class Data(metaclass=DataMeta): return error, msg, messagetype def _archive_composite_dataset( - self, trans, data: DatasetHasHidProtocol, headers: Headers, do_action: str = "zip" + self, trans: "GalaxyWebTransaction", data: DatasetHasHidProtocol, headers: Headers, do_action: str = "zip" ) -> tuple[ZipstreamWrapper | str, Headers]: # save a composite object into a compressed archive for downloading assert data.name @@ -518,7 +524,7 @@ class Data(metaclass=DataMeta): ) return to_content_disposition(filename) - def _serve_file_download(self, headers, data, trans, to_ext, file_size, **kwd): + def _serve_file_download(self, headers, data, trans: "GalaxyWebTransaction", to_ext, file_size, **kwd): if self.is_archive_download(trans.app.datatypes_registry, data.extension): return self._archive_composite_dataset(trans, data, headers, do_action=kwd.get("do_action", "zip")) else: @@ -529,7 +535,9 @@ class Data(metaclass=DataMeta): headers["Content-Disposition"] = self.download_content_disposition(data, to_ext, **kwd) return open(data.get_file_name(auth=ObjectStoreAuth(user=trans.user)), "rb"), headers - def _serve_binary_file_contents_as_text(self, trans, data, headers, file_size, max_peek_size): + def _serve_binary_file_contents_as_text( + self, trans: "ProvidesUserContext", data, headers, file_size, max_peek_size + ): # Use text/plain so the browser preserves whitespace and line endings # and does not attempt to interpret stray markup as HTML. headers["content-type"] = "text/plain; charset=utf-8" @@ -538,7 +546,7 @@ class Data(metaclass=DataMeta): with open(data.get_file_name(auth=ObjectStoreAuth(user=trans.user)), "rb") as fh: return unicodify(fh.read(max_peek_size)), headers - def _serve_file_contents(self, trans, data, headers, preview, file_size, max_peek_size): + def _serve_file_contents(self, trans: "ProvidesUserContext", data, headers, preview, file_size, max_peek_size): from galaxy.datatypes import images preview = util.string_as_bool(preview) @@ -559,7 +567,7 @@ class Data(metaclass=DataMeta): def display_data( self, - trans, + trans: "GalaxyWebTransaction", dataset: DatasetHasHidProtocol, preview: bool = False, filename: str | None = None, @@ -671,7 +679,9 @@ class Data(metaclass=DataMeta): result += indicate_data_truncated() return result - def _yield_user_file_content(self, trans, from_dataset: HasCreatingJob, filename: str, headers: Headers) -> IO: + def _yield_user_file_content( + self, trans: "ProvidesAppContext", from_dataset: HasCreatingJob, filename: str, headers: Headers + ) -> IO: """This method sets the content type header to text/plain if we don't trust html content.""" if trans.app.config.sanitize_all_html and headers.get("content-type", None) == "text/html": # Check to see if this dataset's parent job is allowlisted @@ -790,7 +800,9 @@ class Data(metaclass=DataMeta): ) -> Union["DisplayApplication", None]: return self.display_applications.get(key, default) - def get_display_applications_by_dataset(self, dataset: DatasetProtocol, trans) -> dict[str, "DisplayApplication"]: + def get_display_applications_by_dataset( + self, dataset: DatasetProtocol, trans: "GalaxyWebTransaction" + ) -> dict[str, "DisplayApplication"]: rval = {} for key, value in self.display_applications.items(): value = value.filter_by_dataset(dataset, trans) @@ -851,7 +863,7 @@ class Data(metaclass=DataMeta): return datatypes_registry.get_converters_by_datatype(original_dataset.ext) def find_conversion_destination( - self, dataset: DatasetProtocol, accepted_formats: list[str], datatypes_registry, **kwd + self, dataset: DatasetProtocol, accepted_formats: Iterable[Union[str, "Data"]], datatypes_registry, **kwd ) -> tuple[bool, str | None, Any]: """Returns ( direct_match, converted_ext, existing converted dataset )""" return datatypes_registry.find_conversion_destination_for_dataset_by_extensions( @@ -860,8 +872,8 @@ class Data(metaclass=DataMeta): def convert_dataset( self, - trans, - original_dataset: DatasetHasHidProtocol, + trans: "ProvidesUserContext", + original_dataset: DatasetProtocol, target_type: str, return_output: bool = False, visible: bool = True, @@ -909,6 +921,9 @@ class Data(metaclass=DataMeta): value.visible = False if return_output: return converted_datasets + # Only this message names the dataset by hid; library datasets do not have one + # and reach conversion through the return_output callers instead. + assert isinstance(original_dataset, HasHid) return f"The file conversion of {converter.name} on data {original_dataset.hid} has been added to the Queue." # We need to clear associated files before we set metadata @@ -1048,7 +1063,7 @@ class Data(metaclass=DataMeta): dataset_source = p_dataproviders.dataset.DatasetDataProvider(dataset) return p_dataproviders.chunk.Base64ChunkDataProvider(dataset_source, **settings) - def _clean_and_set_mime_type(self, trans, mime: str, headers: Headers) -> None: + def _clean_and_set_mime_type(self, trans: "ProvidesAppContext", mime: str, headers: Headers) -> None: if mime.lower() in XSS_VULNERABLE_MIME_TYPES: if not getattr(trans.app.config, "serve_xss_vulnerable_mimetypes", True): mime = DEFAULT_MIME_TYPE @@ -1330,7 +1345,7 @@ class ZarrDirectory(Directory): def display_data( self, - trans, + trans: "GalaxyWebTransaction", dataset: DatasetHasHidProtocol, preview: bool = False, filename: str | None = None, diff --git a/lib/galaxy/datatypes/display_applications/util.py b/lib/galaxy/datatypes/display_applications/util.py index a3e6a1b3d00..b33c147ba53 100644 --- a/lib/galaxy/datatypes/display_applications/util.py +++ b/lib/galaxy/datatypes/display_applications/util.py @@ -1,11 +1,16 @@ +from typing import TYPE_CHECKING + from galaxy.model import ( HistoryDatasetAssociation, User, ) from galaxy.security.idencoding import IdEncodingHelper +if TYPE_CHECKING: + from galaxy.managers.context import ProvidesAppContext -def encode_dataset_user(trans, dataset, user): + +def encode_dataset_user(trans: "ProvidesAppContext", dataset, user): # encode dataset id as usual # encode user id using the dataset create time as the key dataset_hash = trans.security.encode_id(dataset.id) @@ -17,7 +22,7 @@ def encode_dataset_user(trans, dataset, user): return dataset_hash, user_hash -def decode_dataset_user(trans, dataset_hash, user_hash): +def decode_dataset_user(trans: "ProvidesAppContext", dataset_hash, user_hash): # decode dataset id as usual # decode user id using the dataset create time as the key dataset_id = trans.security.decode_id(dataset_hash) diff --git a/lib/galaxy/datatypes/protocols.py b/lib/galaxy/datatypes/protocols.py index 91eb1932f70..6dbfcaecd7f 100644 --- a/lib/galaxy/datatypes/protocols.py +++ b/lib/galaxy/datatypes/protocols.py @@ -4,7 +4,10 @@ Location of protocols used in datatypes from typing import Any -from typing_extensions import Protocol +from typing_extensions import ( + Protocol, + runtime_checkable, +) from galaxy.objectstore import ObjectStoreAuth @@ -36,6 +39,7 @@ class HasFileName(Protocol): def get_file_name(self, sync_cache=True, auth: ObjectStoreAuth | None = None) -> str: ... +@runtime_checkable class HasHid(Protocol): hid: str diff --git a/lib/galaxy/datatypes/tabular.py b/lib/galaxy/datatypes/tabular.py index 136f98962f4..5c6cbaec417 100644 --- a/lib/galaxy/datatypes/tabular.py +++ b/lib/galaxy/datatypes/tabular.py @@ -14,6 +14,7 @@ import tempfile from json import dumps from typing import ( cast, + TYPE_CHECKING, ) import pysam @@ -74,6 +75,10 @@ from galaxy.util.markdown import ( ) from . import dataproviders +if TYPE_CHECKING: + from galaxy.managers.context import ProvidesAppContext + from galaxy.webapps.base.webapp import GalaxyWebTransaction + log = logging.getLogger(__name__) MAX_DATA_LINES = 100000 @@ -142,7 +147,9 @@ class TabularData(Text): except Exception: return False - def get_chunk(self, trans, dataset: HasFileName, offset: int = 0, ck_size: int | None = None) -> str: + def get_chunk( + self, trans: "ProvidesAppContext", dataset: HasFileName, offset: int = 0, ck_size: int | None = None + ) -> str: ck_data, last_read = self._read_chunk(trans, dataset, offset, ck_size) return dumps( { @@ -152,7 +159,7 @@ class TabularData(Text): } ) - def _read_chunk(self, trans, dataset: HasFileName, offset: int, ck_size: int | None = None): + def _read_chunk(self, trans: "ProvidesAppContext", dataset: HasFileName, offset: int, ck_size: int | None = None): with compression_utils.get_fileobj(dataset.get_file_name()) as f: f.seek(offset) try: @@ -173,7 +180,7 @@ class TabularData(Text): def display_data( self, - trans, + trans: "GalaxyWebTransaction", dataset: DatasetHasHidProtocol, preview: bool = False, filename: str | None = None, @@ -1631,7 +1638,9 @@ class ConnectivityTable(Tabular): i += 1 return False - def get_chunk(self, trans, dataset: HasFileName, offset: int = 0, ck_size: int | None = None) -> str: + def get_chunk( + self, trans: "ProvidesAppContext", dataset: HasFileName, offset: int = 0, ck_size: int | None = None + ) -> str: ck_data, last_read = self._read_chunk(trans, dataset, offset, ck_size) try: # The ConnectivityTable format has several derivatives of which one is delimited by (multiple) spaces. diff --git a/lib/galaxy/datatypes/text.py b/lib/galaxy/datatypes/text.py index b92a2bba317..f6383671472 100644 --- a/lib/galaxy/datatypes/text.py +++ b/lib/galaxy/datatypes/text.py @@ -10,6 +10,7 @@ import subprocess import tempfile from typing import ( IO, + TYPE_CHECKING, ) import ijson @@ -42,6 +43,13 @@ from galaxy.util import ( unicodify, ) +if TYPE_CHECKING: + from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesUserContext, + ) + from galaxy.webapps.base.webapp import GalaxyWebTransaction + log = logging.getLogger(__name__) @@ -207,7 +215,7 @@ class Ipynb(Json): def display_data( self, - trans, + trans: "GalaxyWebTransaction", dataset: DatasetHasHidProtocol, preview: bool = False, filename: str | None = None, @@ -223,7 +231,7 @@ class Ipynb(Json): def _display_data_trusted( self, - trans, + trans: "ProvidesUserContext", dataset: DatasetHasHidProtocol, preview: bool = False, filename: str | None = None, @@ -1280,7 +1288,9 @@ class Yaml(Text): """Returns the mime type of the datatype""" return "application/yaml" - def _yield_user_file_content(self, trans, from_dataset: HasCreatingJob, filename: str, headers: Headers) -> IO: + def _yield_user_file_content( + self, trans: "ProvidesAppContext", from_dataset: HasCreatingJob, filename: str, headers: Headers + ) -> IO: # Override non-standard application/yaml mediatype with # text/plain, so preview is shown in preview iframe, # instead of downloading the file. diff --git a/lib/galaxy/exceptions/error_codes.py b/lib/galaxy/exceptions/error_codes.py index 7daed2bcef0..82504b56d3b 100644 --- a/lib/galaxy/exceptions/error_codes.py +++ b/lib/galaxy/exceptions/error_codes.py @@ -51,3 +51,16 @@ for entry in loads(error_codes_json): globals()[name] = error_code_obj error_codes_by_name[name] = error_code_obj error_codes_by_int_code[error_code_obj.code] = error_code_obj + + +def __getattr__(name: str) -> ErrorCode: + """Expose the JSON-driven, dynamically-created error codes to static analysis. + + The codes above are assigned via ``globals()[name] = ...``, which mypy cannot + see, so attribute access like ``error_codes.ADMIN_REQUIRED`` is otherwise + reported as an unknown attribute. + """ + try: + return error_codes_by_name[name] + except KeyError: + raise AttributeError(name) diff --git a/lib/galaxy/files/__init__.py b/lib/galaxy/files/__init__.py index 77bc0803bcd..e25948ee0ca 100644 --- a/lib/galaxy/files/__init__.py +++ b/lib/galaxy/files/__init__.py @@ -26,6 +26,29 @@ from .plugins import ( FileSourcePluginsConfig, ) + +class ProvidesFileSourcesTransaction(Protocol): + """The slice of a Galaxy transaction ProvidesFileSourcesUserContext reads.""" + + @property + def anonymous(self) -> bool: ... + + @property + def user(self) -> Any: ... + + @property + def user_ftp_dir(self) -> str | None: ... + + @property + def user_is_admin(self) -> bool: ... + + @property + def user_vault(self) -> Any: ... + + @property + def app(self) -> Any: ... + + log = logging.getLogger(__name__) @@ -371,7 +394,7 @@ OptionalUserContext = FileSourcesUserContext | None class ProvidesFileSourcesUserContext(FileSourcesUserContext, FileSourceDictifiable): """Implement a FileSourcesUserContext from a Galaxy ProvidesUserContext (e.g. trans).""" - def __init__(self, trans, **kwargs): + def __init__(self, trans: ProvidesFileSourcesTransaction, **kwargs): self.trans = trans @property diff --git a/lib/galaxy/managers/agents.py b/lib/galaxy/managers/agents.py index 29740ebdca8..a10123e5616 100644 --- a/lib/galaxy/managers/agents.py +++ b/lib/galaxy/managers/agents.py @@ -9,10 +9,10 @@ from galaxy.agents import GalaxyAgentDependencies from galaxy.agents.registry import AgentRegistry from galaxy.agents.router import QueryRouterAgent from galaxy.config import GalaxyAppConfiguration -from galaxy.managers.context import ProvidesUserContext from galaxy.managers.jobs import JobManager from galaxy.model import User from galaxy.schema.agents import AgentResponse +from galaxy.work.context import SessionRequestContext log = logging.getLogger(__name__) @@ -30,7 +30,7 @@ class AgentService: self.job_manager = job_manager self.registry = registry - def create_dependencies(self, trans: ProvidesUserContext, user: User) -> GalaxyAgentDependencies: + def create_dependencies(self, trans: SessionRequestContext, user: User) -> GalaxyAgentDependencies: """Create agent dependencies for dependency injection.""" toolbox = trans.app.toolbox if hasattr(trans, "app") and hasattr(trans.app, "toolbox") else None return GalaxyAgentDependencies( @@ -47,7 +47,7 @@ class AgentService: self, agent_type: str, query: str, - trans: ProvidesUserContext, + trans: SessionRequestContext, user: User, context: dict[str, Any] | None = None, ) -> AgentResponse: @@ -96,7 +96,7 @@ class AgentService: async def route_and_execute( self, query: str, - trans: ProvidesUserContext, + trans: SessionRequestContext, user: User, context: dict[str, Any] | None = None, agent_type: str = "auto", diff --git a/lib/galaxy/managers/base.py b/lib/galaxy/managers/base.py index e3df51c2668..d852fa007bb 100644 --- a/lib/galaxy/managers/base.py +++ b/lib/galaxy/managers/base.py @@ -65,7 +65,7 @@ from galaxy.structured_app import ( ) if TYPE_CHECKING: - from galaxy.managers.context import ProvidesAppContext + from galaxy.managers.context import ProvidesUserContext log = logging.getLogger(__name__) @@ -84,7 +84,7 @@ FunctionFilterParsersType = dict[str, Any] # ==== accessors from base/controller.py -def security_check(trans, item, check_ownership=False, check_accessible=False): +def security_check(trans: "ProvidesUserContext", item, check_ownership=False, check_accessible=False): """ Security checks for an item: checks if (a) user owns item or (b) item is accessible to user. This is a generic method for dealing with objects @@ -162,7 +162,7 @@ def encode_with_security(security: IdEncodingHelper, id: Any, kind: str | None = def get_object( - trans: "ProvidesAppContext", + trans: "ProvidesUserContext", id, class_name, check_ownership: bool = False, diff --git a/lib/galaxy/managers/collections.py b/lib/galaxy/managers/collections.py index 8cf82251efa..63693da5f4c 100644 --- a/lib/galaxy/managers/collections.py +++ b/lib/galaxy/managers/collections.py @@ -177,6 +177,54 @@ class DatasetCollectionManager: return dataset_collection + @overload + def create( + self, + trans: ProvidesHistoryContext, + parent: model.History, + name, + collection_type, + element_identifiers=None, + elements=None, + implicit_collection_info=None, + trusted_identifiers=None, + hide_source_items: bool = False, + tags=None, + copy_elements: bool = False, + history=None, + set_hid: bool = True, + flush=True, + completed_job=None, + output_name=None, + fields: str | list["FieldDict"] | None = None, + column_definitions=None, + rows=None, + ) -> model.HistoryDatasetCollectionAssociation: ... + + @overload + def create( + self, + trans: ProvidesHistoryContext, + parent: model.LibraryFolder, + name, + collection_type, + element_identifiers=None, + elements=None, + implicit_collection_info=None, + trusted_identifiers=None, + hide_source_items: bool = False, + tags=None, + copy_elements: bool = False, + history=None, + set_hid: bool = True, + flush=True, + completed_job=None, + output_name=None, + fields: str | list["FieldDict"] | None = None, + column_definitions=None, + rows=None, + ) -> model.LibraryDatasetCollectionAssociation: ... + def create( self, trans: ProvidesHistoryContext, @@ -603,7 +651,12 @@ class DatasetCollectionManager: ) def __recursively_create_collections_for_identifiers( - self, trans, element_identifiers, hide_source_items: bool, copy_elements: bool, history=None + self, + trans: ProvidesHistoryContext, + element_identifiers, + hide_source_items: bool, + copy_elements: bool, + history=None, ): for element_identifier in element_identifiers: try: @@ -629,7 +682,7 @@ class DatasetCollectionManager: return element_identifiers def __recursively_create_collections_for_elements( - self, trans, elements, hide_source_items: bool, copy_elements: bool, history=None + self, trans: ProvidesHistoryContext, elements, hide_source_items: bool, copy_elements: bool, history=None ) -> None: if elements is self.ELEMENTS_UNINITIALIZED: return @@ -655,7 +708,12 @@ class DatasetCollectionManager: elements.update(new_elements) def __load_elements( - self, trans, element_identifiers, hide_source_items: bool = False, copy_elements: bool = False, history=None + self, + trans: ProvidesHistoryContext, + element_identifiers, + hide_source_items: bool = False, + copy_elements: bool = False, + history=None, ) -> dict[str, HDCAElementObjectType]: elements: dict[str, HDCAElementObjectType] = {} for element_identifier in element_identifiers: @@ -669,7 +727,12 @@ class DatasetCollectionManager: return elements def __load_element( - self, trans, element_identifier, hide_source_items: bool, copy_elements: bool, history=None + self, + trans: ProvidesHistoryContext, + element_identifier, + hide_source_items: bool, + copy_elements: bool, + history=None, ) -> HDCAElementObjectType: # if not isinstance( element_identifier, dict ): # # Is allowing this to just be the id of an hda too clever? Somewhat @@ -752,7 +815,7 @@ class DatasetCollectionManager: return self.__get_library_collection_instance(trans, id, **kwds) raise NotImplementedError() - def get_dataset_collection(self, trans, encoded_id): + def get_dataset_collection(self, trans: ProvidesAppContext, encoded_id): collection_id = int(trans.app.security.decode_id(encoded_id)) collection = trans.sa_session.get(DatasetCollection, collection_id) return collection diff --git a/lib/galaxy/managers/context.py b/lib/galaxy/managers/context.py index 30f8ed17bce..67179a6d094 100644 --- a/lib/galaxy/managers/context.py +++ b/lib/galaxy/managers/context.py @@ -46,6 +46,8 @@ from typing import ( Any, cast, Literal, + Optional, + TYPE_CHECKING, ) from sqlalchemy import select @@ -69,6 +71,9 @@ from galaxy.security.vault import UserVaultWrapper from galaxy.structured_app import MinimalManagerApp from galaxy.util import bunch +if TYPE_CHECKING: + from galaxy.tools.parameters.dataset_matcher import DatasetMatcherFactory + class ProvidesAppContext: """For transaction-like objects to provide Galaxy convenience layer for @@ -314,6 +319,18 @@ class ProvidesHistoryContext(ProvidesUserContext): properties. """ + # set per-request by galaxy.tools.parameters.dataset_matcher while a tool + # form is being evaluated + dataset_matcher_factory: Optional["DatasetMatcherFactory"] = None + + @abc.abstractmethod + def get_history(self, create: bool = False) -> History | None: + """Return the current history, optionally creating one when there is none. + + Transactions do not always have an active history, so None is a valid + response even when create is set. + """ + @property @abc.abstractmethod def history(self) -> History | None: diff --git a/lib/galaxy/managers/dataset_storage_operations.py b/lib/galaxy/managers/dataset_storage_operations.py index b8de87ed431..9ac24f8033c 100644 --- a/lib/galaxy/managers/dataset_storage_operations.py +++ b/lib/galaxy/managers/dataset_storage_operations.py @@ -981,7 +981,6 @@ class StorageOperationRunExecutor: self.run = run self.user = user self.current_task_id = current_task_id - self.trans = SimpleNamespace(user=user) self.storage_operation_manager = storage_operation_manager self.quota_source_map = app.object_store.get_quota_source_map() self.target_quota_usage_at_start = 0 @@ -1315,7 +1314,9 @@ class StorageOperationRunExecutor: # Queue cleanup for after DB commit to ensure crash safety. self._pending_cleanups.append((source_proxy, extra_files_path_name)) else: - self.dataset_manager.update_object_store_id(self.trans, dataset, self.run.target_object_store_id) + self.dataset_manager.update_object_store_id_for_user( + self.user, dataset, self.run.target_object_store_id + ) self.additional_target_usage += quota_delta self.succeeded_count += 1 diff --git a/lib/galaxy/managers/datasets.py b/lib/galaxy/managers/datasets.py index 0fb23808455..079c4f7ee1c 100644 --- a/lib/galaxy/managers/datasets.py +++ b/lib/galaxy/managers/datasets.py @@ -25,12 +25,17 @@ from galaxy.managers import ( secured, users, ) +from galaxy.managers.context import ( + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.model import ( Dataset, DatasetHash, DatasetInstance, DatasetPermissions, HistoryDatasetAssociation, + User, ) from galaxy.model.db.role import ( get_private_role_user_emails_dict, @@ -133,7 +138,10 @@ class DatasetManager( roles = user.all_roles_exploiting_cache() if user else [] return self.app.security_agent.can_access_dataset(roles, dataset) - def update_object_store_id(self, trans, dataset, object_store_id: str): + def update_object_store_id(self, trans: ProvidesUserContext, dataset, object_store_id: str): + return self.update_object_store_id_for_user(trans.user, dataset, object_store_id) + + def update_object_store_id_for_user(self, user: User | None, dataset, object_store_id: str): device_source_map = self.app.object_store.get_device_source_map() old_object_store_id = dataset.object_store_id new_object_store_id = object_store_id @@ -146,7 +154,7 @@ class DatasetManager( "Cannot swap object store IDs for object stores that don't share a device ID." ) - if not self.app.security_agent.can_change_object_store_id(trans.user, dataset): + if not self.app.security_agent.can_change_object_store_id(user, dataset): # TODO: probably want separate exceptions for doesn't own the dataset and dataset # has been shared. raise exceptions.InsufficientPermissionsException("Cannot change dataset permissions...") @@ -228,7 +236,7 @@ class DatasetRBACPermissions: self.manage = rbac_secured.ManageDatasetRBACPermission(app) # TODO: temporary facade over security_agent - def available_roles(self, trans, dataset, controller="root"): + def available_roles(self, trans: ProvidesUserContext, dataset, controller="root"): return self.app.security_agent.get_legitimate_roles(trans, dataset, controller) def get(self, dataset, flush=True): @@ -494,7 +502,7 @@ class DatasetAssociationManager( rval["modify_item_roles"] = role_name_id_pairs(modify_roles, private_role_emails, encode_id) return rval - def ensure_dataset_on_disk(self, trans, dataset: U): + def ensure_dataset_on_disk(self, trans: ProvidesUserContext, dataset: U): # Not a guarantee data is really present, but excludes a lot of expected cases if not dataset.dataset: raise exceptions.InternalServerError("Item has no associated dataset.") @@ -546,7 +554,7 @@ class DatasetAssociationManager( ) return True - def detect_datatype(self, trans, dataset_assoc: U): + def detect_datatype(self, trans: ProvidesHistoryContext, dataset_assoc: U): """Sniff and assign the datatype to a given dataset association (ldda or hda)""" session = self.session() self.ensure_can_change_datatype(dataset_assoc) @@ -558,7 +566,9 @@ class DatasetAssociationManager( session.commit() self.set_metadata(trans, dataset_assoc) - def set_metadata(self, trans, dataset_assoc: U, overwrite: bool = False, validate: bool = True) -> None: + def set_metadata( + self, trans: ProvidesHistoryContext, dataset_assoc: U, overwrite: bool = False, validate: bool = True + ) -> None: """Trigger a job that detects and sets metadata on a given dataset association (ldda or hda)""" self.ensure_can_set_metadata(dataset_assoc) if overwrite: @@ -579,7 +589,7 @@ class DatasetAssociationManager( if spec.get("default"): setattr(data.metadata, name, spec.unwrap(spec.get("default"))) - def update_permissions(self, trans, dataset_assoc: U, **kwd): + def update_permissions(self, trans: ProvidesUserContext, dataset_assoc: U, **kwd): action = kwd.get("action", "set_permissions") if action not in ["remove_restrictions", "make_private", "set_permissions"]: raise exceptions.RequestParameterInvalidException( @@ -632,7 +642,7 @@ class DatasetAssociationManager( self._set_permissions(trans, dataset_assoc, role_ids_dict) - def _set_permissions(self, trans, dataset_assoc: U, roles_dict): + def _set_permissions(self, trans: ProvidesUserContext, dataset_assoc: U, roles_dict): raise exceptions.NotImplemented() diff --git a/lib/galaxy/managers/display_applications.py b/lib/galaxy/managers/display_applications.py index 3ee45d0a2b1..e656b7d0506 100644 --- a/lib/galaxy/managers/display_applications.py +++ b/lib/galaxy/managers/display_applications.py @@ -204,7 +204,9 @@ class DisplayApplicationsManager: ) raise MessageException("You do not have permission to view this dataset at an external display application.") - def _can_access_dataset(self, trans, dataset_association, allow_admin=True, additional_roles=None): + def _can_access_dataset( + self, trans: ProvidesUserContext, dataset_association, allow_admin=True, additional_roles=None + ): roles = trans.get_current_user_roles() if additional_roles: roles = roles + additional_roles diff --git a/lib/galaxy/managers/executables.py b/lib/galaxy/managers/executables.py index 86a250ef6f9..054213f489a 100644 --- a/lib/galaxy/managers/executables.py +++ b/lib/galaxy/managers/executables.py @@ -7,15 +7,16 @@ from typing import ( import yaml from galaxy import exceptions +from galaxy.managers.context import ProvidesUserContext from galaxy.util import in_directory -def artifact_class(trans, as_dict: dict[str, Any], allow_in_directory: str | None = None): +def artifact_class(trans: ProvidesUserContext | None, as_dict: dict[str, Any], allow_in_directory: str | None = None): object_id = as_dict.get("object_id", None) if as_dict.get("src", None) == "from_path": workflow_path = as_dict.get("path") allow = not trans or trans.user_is_admin - allow = allow or (allow_in_directory and in_directory(workflow_path, allow_in_directory)) + allow = allow or bool(allow_in_directory and in_directory(workflow_path, allow_in_directory)) if not allow: raise exceptions.AdminRequiredException() diff --git a/lib/galaxy/managers/file_source_instances.py b/lib/galaxy/managers/file_source_instances.py index 5b66548115c..d666b9f4151 100644 --- a/lib/galaxy/managers/file_source_instances.py +++ b/lib/galaxy/managers/file_source_instances.py @@ -563,7 +563,7 @@ class FileSourceInstancesManager: def _save(self, user_file_source: UserFileSource) -> None: save_template_instance(self._sa_session, user_file_source) - def _to_model(self, trans, persisted_file_source: UserFileSource) -> UserFileSourceModel: + def _to_model(self, trans: ProvidesUserContext, persisted_file_source: UserFileSource) -> UserFileSourceModel: file_source_type = persisted_file_source.template.configuration.type secrets = persisted_file_source.template_secrets or [] uuid = str(persisted_file_source.uuid) diff --git a/lib/galaxy/managers/folders.py b/lib/galaxy/managers/folders.py index 3ce08e7fb83..ac35b6bbd0f 100644 --- a/lib/galaxy/managers/folders.py +++ b/lib/galaxy/managers/folders.py @@ -54,7 +54,10 @@ from galaxy.schema.schema import LibraryFolderContentsIndexQueryPayload from galaxy.security import RBACAgent if TYPE_CHECKING: - from galaxy.managers.context import ProvidesUserContext + from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesUserContext, + ) log = logging.getLogger(__name__) @@ -63,7 +66,7 @@ log = logging.getLogger(__name__) class SecurityParams: """Contains security data bundled for reusability.""" - user_role_ids: list[model.Role] + user_role_ids: list[int] security_agent: RBACAgent is_admin: bool @@ -148,7 +151,7 @@ class FolderManager: folder = self.check_accessible(trans, folder) return folder - def check_modifyable(self, trans, folder): + def check_modifyable(self, trans: "ProvidesUserContext", folder): """ Check whether the user can modify the folder (name and description). @@ -165,7 +168,7 @@ class FolderManager: else: return folder - def check_manageable(self, trans, folder): + def check_manageable(self, trans: "ProvidesUserContext", folder): """ Check whether the user can manage the folder. @@ -182,14 +185,14 @@ class FolderManager: else: return folder - def check_accessible(self, trans, folder): + def check_accessible(self, trans: "ProvidesUserContext", folder): """ Check whether the folder is accessible to current user. By default every folder is accessible (contents have their own permissions). """ return folder - def get_folder_dict(self, trans, folder): + def get_folder_dict(self, trans: "ProvidesUserContext", folder): """ Return folder data in the form of a dictionary. @@ -204,7 +207,13 @@ class FolderManager: folder_dict["update_time"] = folder.update_time return folder_dict - def create(self, trans, parent_folder_id: int, new_folder_name: str, new_folder_description: str | None = None): + def create( + self, + trans: "ProvidesUserContext", + parent_folder_id: int, + new_folder_name: str, + new_folder_description: str | None = None, + ): """ Create a new folder under the given folder. @@ -240,7 +249,7 @@ class FolderManager: trans.app.security_agent.copy_library_permissions(trans, parent_folder, new_folder) return new_folder - def update(self, trans, folder, name=None, description=None): + def update(self, trans: "ProvidesUserContext", folder, name=None, description=None): """ Update the given folder's name or description. @@ -272,7 +281,7 @@ class FolderManager: trans.sa_session.commit() return folder - def delete(self, trans, folder, undelete=False): + def delete(self, trans: "ProvidesUserContext", folder, undelete=False): """ Mark given folder deleted/undeleted based on the flag. @@ -296,7 +305,7 @@ class FolderManager: trans.sa_session.commit() return folder - def get_current_roles(self, trans, folder): + def get_current_roles(self, trans: "ProvidesUserContext", folder): """ Find all roles currently connected to relevant permissions on the folder. @@ -332,7 +341,7 @@ class FolderManager: add_library_item_role_list=role_name_id_pairs(add_roles, private_role_emails, encode_id), ) - def can_add_item(self, trans, folder): + def can_add_item(self, trans: "ProvidesUserContext", folder): """ Return true if the user has permissions to add item to the given folder. """ @@ -367,7 +376,7 @@ class FolderManager: raise MalformedId(f"Malformed folder id ( {str(encoded_folder_id)} ) specified, unable to decode.") return cut_id - def decode_folder_id(self, trans, encoded_folder_id): + def decode_folder_id(self, trans: "ProvidesAppContext", encoded_folder_id): """ Decode the folder id given that it has already lost the prefixed 'F'. @@ -381,7 +390,7 @@ class FolderManager: """ return trans.security.decode_id(encoded_folder_id, object_name="folder") - def cut_and_decode(self, trans, encoded_folder_id): + def cut_and_decode(self, trans: "ProvidesAppContext", encoded_folder_id): """ Cuts the folder prefix (the prepended 'F') and returns the decoded id. @@ -395,7 +404,7 @@ class FolderManager: def get_contents( self, - trans, + trans: "ProvidesUserContext", folder: LibraryFolder, payload: LibraryFolderContentsIndexQueryPayload, ) -> tuple[list[LibraryFolder | LibraryDataset], int]: diff --git a/lib/galaxy/managers/forms.py b/lib/galaxy/managers/forms.py index d8da000445d..1b6011c22ec 100644 --- a/lib/galaxy/managers/forms.py +++ b/lib/galaxy/managers/forms.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING + from sqlalchemy import select from sqlalchemy.exc import ( MultipleResultsFound, @@ -17,6 +19,9 @@ from galaxy.model import ( ) from galaxy.util import unicodify +if TYPE_CHECKING: + from galaxy.webapps.base.webapp import GalaxyWebTransaction + def get_form_definitions(session): stmt = select(FormDefinition) @@ -33,7 +38,7 @@ def get_filtered_form_definitions_current(session, filter): return session.scalars(stmt) -def get_form(trans, form_id): +def get_form(trans: "GalaxyWebTransaction", form_id): """Get a FormDefinition from the database by id.""" form = trans.sa_session.query(FormDefinitionCurrent).get(trans.security.decode_id(form_id)) if not form: diff --git a/lib/galaxy/managers/genomes.py b/lib/galaxy/managers/genomes.py index feb3f45ec36..0686c08db95 100644 --- a/lib/galaxy/managers/genomes.py +++ b/lib/galaxy/managers/genomes.py @@ -12,7 +12,10 @@ from galaxy.exceptions import ( ReferenceDataError, RequestParameterInvalidException, ) -from galaxy.managers.context import ProvidesUserContext +from galaxy.managers.context import ( + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.model import User from galaxy.model.database_utils import is_postgres from galaxy.structured_app import ( @@ -41,7 +44,7 @@ class GenomesManager: return False def get_genome( - self, trans: ProvidesUserContext, id: str, num: int, chrom: str, low: int, high: int, reference: bool + self, trans: ProvidesHistoryContext, id: str, num: int, chrom: str, low: int, high: int, reference: bool ) -> Any: if reference: region = self.genomes.reference(trans, dbkey=id, chrom=chrom, low=low, high=high) diff --git a/lib/galaxy/managers/groups.py b/lib/galaxy/managers/groups.py index 413db99c119..0a6b6eabd67 100644 --- a/lib/galaxy/managers/groups.py +++ b/lib/galaxy/managers/groups.py @@ -19,6 +19,7 @@ from galaxy.schema.groups import ( GroupUpdatePayload, ) from galaxy.structured_app import MinimalManagerApp +from galaxy.work.context import SessionRequestContext class GroupsManager: @@ -27,7 +28,7 @@ class GroupsManager: def __init__(self, app: MinimalManagerApp) -> None: self._app = app - def index(self, trans: ProvidesAppContext): + def index(self, trans: SessionRequestContext): """ Displays a collection (list) of groups. """ @@ -39,7 +40,7 @@ class GroupsManager: rval.append(item) return rval - def create(self, trans: ProvidesAppContext, payload: GroupCreatePayload): + def create(self, trans: SessionRequestContext, payload: GroupCreatePayload): """ Creates a new group. """ @@ -73,7 +74,7 @@ class GroupsManager: item["url"] = self._url_for(trans, "group", id=encoded_id) return [item] - def show(self, trans: ProvidesAppContext, group_id: int): + def show(self, trans: SessionRequestContext, group_id: int): """ Displays information about a group. """ @@ -85,7 +86,7 @@ class GroupsManager: item["roles_url"] = self._url_for(trans, "group_roles", group_id=encoded_id) return item - def update(self, trans: ProvidesAppContext, group_id: int, payload: GroupUpdatePayload): + def update(self, trans: SessionRequestContext, group_id: int, payload: GroupUpdatePayload): """ Modifies a group. """ @@ -138,7 +139,7 @@ class GroupsManager: trans.sa_session.add(group) trans.sa_session.commit() - def _url_for(self, trans, name, **kwargs): + def _url_for(self, trans: SessionRequestContext, name, **kwargs): return trans.url_builder(name, **kwargs) def _check_duplicated_group_name(self, sa_session: galaxy_scoped_session, group_name: str) -> None: diff --git a/lib/galaxy/managers/hdas.py b/lib/galaxy/managers/hdas.py index 2c13405467c..cba7b2c88f6 100644 --- a/lib/galaxy/managers/hdas.py +++ b/lib/galaxy/managers/hdas.py @@ -44,7 +44,11 @@ from galaxy.managers import ( taggable, users, ) -from galaxy.managers.context import ProvidesHistoryContext +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.model import ( HistoryDatasetAssociation, HistoryDatasetCollectionAssociation, @@ -79,7 +83,10 @@ from galaxy.tool_util_models.parameters import ( FileRequestUri, ) from galaxy.util.compression_utils import get_fileobj -from galaxy.work.context import WorkRequestContext +from galaxy.work.context import ( + SessionRequestContext, + WorkRequestContext, +) if TYPE_CHECKING: from galaxy.model import LibraryDatasetDatasetAssociation @@ -339,7 +346,7 @@ class HDAManager( # override to scope to history owner return self._user_annotation(hda, hda.user) - def _set_permissions(self, trans, hda, role_ids_dict): + def _set_permissions(self, trans: ProvidesUserContext, hda, role_ids_dict): # The user associated the DATASET_ACCESS permission on the dataset with 1 or more roles. We # need to ensure that they did not associate roles that would cause accessibility problems. security_agent = trans.app.security_agent @@ -657,7 +664,7 @@ class HDASerializer( # datasets._UnflattenedMetadataDatasetAssociationSerialize keys = self._view_to_keys("inaccessible") return super().serialize(item, keys, user=user, **context) - def serialize_display_apps(self, item, key, trans=None, **context): + def serialize_display_apps(self, item, key, trans: ProvidesAppContext | None = None, **context): """ Return dictionary containing new-style display app urls. """ @@ -681,7 +688,9 @@ class HDASerializer( # datasets._UnflattenedMetadataDatasetAssociationSerialize return display_apps - def serialize_old_display_applications(self, item, key, trans=None, **context): + # trans arrives through the serializer dispatch's **context, so it has to be optional here; + # a request context is required in practice, for trans.request.base. + def serialize_old_display_applications(self, item, key, trans: "SessionRequestContext | None" = None, **context): """ Return dictionary containing old-style display app urls. """ @@ -692,6 +701,7 @@ class HDASerializer( # datasets._UnflattenedMetadataDatasetAssociationSerialize and hda.state == HistoryDatasetAssociation.states.OK and not hda.deleted ): + assert trans is not None display_link_fn = hda.datatype.get_display_links for display_app in hda.datatype.get_display_types(): target_frame, display_links = display_link_fn( diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py index 48ffe200622..36ce9761368 100644 --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -12,6 +12,7 @@ from typing import ( cast, Literal, TYPE_CHECKING, + Union, ) from uuid import UUID @@ -46,7 +47,10 @@ from galaxy.managers.base import ( SortableManager, StorageCleanerManager, ) -from galaxy.managers.context import ProvidesUserContext +from galaxy.managers.context import ( + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.managers.export_tracker import StoreExportTracker from galaxy.model import ( History, @@ -87,10 +91,17 @@ from galaxy.util.search import ( parse_filters_structured, RawTextTerm, ) +from galaxy.work.context import SessionRequestContext if TYPE_CHECKING: from sqlalchemy.engine import ScalarResult + from galaxy.webapps.base.webapp import GalaxyWebTransaction + +# "Current history" is a request/session-scoped concept: only trans objects backed by +# a session (FastAPI SessionRequestContext) or the legacy web transaction implement it. +CurrentHistoryContext = Union[SessionRequestContext, "GalaxyWebTransaction"] + log = logging.getLogger(__name__) INDEX_SEARCH_FILTERS = { @@ -321,14 +332,14 @@ class HistoryManager(sharable.SharableModelManager[model.History], deletable.Pur # def is_current_users_current_history( self, history, trans ): # pass - def get_current(self, trans): + def get_current(self, trans: CurrentHistoryContext): """ Return the current history. """ # TODO: trans return trans.get_history() - def set_current(self, trans, history): + def set_current(self, trans: CurrentHistoryContext, history): """ Set the current history. """ @@ -336,7 +347,7 @@ class HistoryManager(sharable.SharableModelManager[model.History], deletable.Pur trans.set_history(history) return history - def set_current_by_id(self, trans, history_id): + def set_current_by_id(self, trans: CurrentHistoryContext, history_id): """ Set the current history by an id. """ @@ -382,16 +393,17 @@ class HistoryManager(sharable.SharableModelManager[model.History], deletable.Pur stmt = select(Job).where(Job.history == history).where(Job.state.in_(Job.non_ready_states)) return self.session().scalars(stmt) - def queue_history_import(self, trans, archive_type, archive_source, target_history=None): + def queue_history_import(self, trans: ProvidesHistoryContext, archive_type, archive_source, target_history=None): # Run job to do import. history_imp_tool = trans.app.toolbox.get_tool("__IMPORT_HISTORY__") + assert history_imp_tool is not None incoming = {"__ARCHIVE_SOURCE__": archive_source, "__ARCHIVE_TYPE__": archive_type} job, *_ = history_imp_tool.execute(trans, incoming=incoming, history=target_history) trans.app.job_manager.enqueue(job, tool=history_imp_tool) return job # TODO: remove this function when the legacy endpoint using it is removed - def legacy_serve_ready_history_export(self, trans, jeha): + def legacy_serve_ready_history_export(self, trans: "GalaxyWebTransaction", jeha): assert jeha.ready if jeha.compressed: trans.response.set_content_type("application/x-gzip") @@ -402,7 +414,7 @@ class HistoryManager(sharable.SharableModelManager[model.History], deletable.Pur archive = trans.app.object_store.get_filename(jeha.dataset, auth=ObjectStoreAuth(user=trans.user)) return open(archive, mode="rb") - def get_ready_history_export_file_path(self, trans, jeha) -> str: + def get_ready_history_export_file_path(self, trans: ProvidesUserContext, jeha) -> str: """ Serves the history export archive for use as a streaming response so the file doesn't need to be loaded into memory. @@ -411,7 +423,14 @@ class HistoryManager(sharable.SharableModelManager[model.History], deletable.Pur return trans.app.object_store.get_filename(jeha.dataset, auth=ObjectStoreAuth(user=trans.user)) def queue_history_export( - self, trans, history, gzip=True, include_hidden=False, include_deleted=False, directory_uri=None, file_name=None + self, + trans: ProvidesHistoryContext, + history, + gzip=True, + include_hidden=False, + include_deleted=False, + directory_uri=None, + file_name=None, ): # Convert options to booleans. if isinstance(gzip, str): @@ -437,12 +456,18 @@ class HistoryManager(sharable.SharableModelManager[model.History], deletable.Pur # Run job to do export. history_exp_tool = trans.app.toolbox.get_tool(export_tool_id) + assert history_exp_tool is not None job, *_ = history_exp_tool.execute(trans, incoming=params, history=history) trans.app.job_manager.enqueue(job, tool=history_exp_tool) return job def get_sharing_extra_information( - self, trans, item, users: set[model.User], errors: set[str], option: sharable.SharingOptions | None = None + self, + trans: ProvidesUserContext, + item, + users: set[model.User], + errors: set[str], + option: sharable.SharingOptions | None = None, ) -> ShareHistoryExtra: """Returns optional extra information about the datasets of the history that can be accessed by the users.""" extra = ShareHistoryExtra() @@ -505,7 +530,7 @@ class HistoryManager(sharable.SharableModelManager[model.History], deletable.Pur ) return bool(self.session().scalar(stmt)) - def make_members_public(self, trans, item): + def make_members_public(self, trans: ProvidesUserContext, item): """Make the non-purged datasets in history public. Performs permissions check. """ @@ -686,7 +711,9 @@ class HistoryExportManager: self.app = app self.export_tracker = export_tracker - def get_task_exports(self, trans, history_id: int, limit: int | None = None, offset: int | None = None): + def get_task_exports( + self, trans: ProvidesHistoryContext, history_id: int, limit: int | None = None, offset: int | None = None + ): """Returns task-based exports associated with this history""" history = self._history(trans, history_id) export_associations = self.export_tracker.get_object_exports( @@ -759,17 +786,18 @@ class HistoryExportManager: "export_metadata": export_metadata, } - def get_exports(self, trans, history_id: int): + def get_exports(self, trans: ProvidesHistoryContext, history_id: int): """Returns job-based exports associated with this history""" history = self._history(trans, history_id) matching_exports = history.exports return [self.serialize(trans, history_id, e) for e in matching_exports] - def serialize(self, trans, history_id: int, jeha: model.JobExportHistoryArchive) -> dict: + def serialize(self, trans: ProvidesHistoryContext, history_id: int, jeha: model.JobExportHistoryArchive) -> dict: rval = jeha.to_dict() rval["type"] = "job" encoded_jeha_id = Security.security.encode_id(jeha.id) encoded_history_id = Security.security.encode_id(history_id) + assert trans.url_builder api_url = trans.url_builder("history_archive_download", history_id=encoded_history_id, jeha_id=encoded_jeha_id) external_url = trans.url_builder( "history_archive_download", history_id=encoded_history_id, jeha_id="latest", qualified=True @@ -783,7 +811,9 @@ class HistoryExportManager: rval = trans.security.encode_all_ids(rval) return rval - def get_ready_jeha(self, trans, history_id: int, jeha_id: int | Literal["latest"] = "latest"): + def get_ready_jeha( + self, trans: ProvidesHistoryContext, history_id: int, jeha_id: int | Literal["latest"] = "latest" + ): history = self._history(trans, history_id) matching_exports = history.exports if jeha_id != "latest": @@ -797,7 +827,7 @@ class HistoryExportManager: return jeha - def _history(self, trans, history_id: int) -> model.History: + def _history(self, trans: ProvidesHistoryContext, history_id: int) -> model.History: history = self.app.history_manager.get_accessible(history_id, trans.user, current_history=trans.history) return history @@ -995,7 +1025,7 @@ class HistorySerializer(sharable.SharableModelSerializer, deletable.PurgableSeri return state - def serialize_contents(self, item, key, trans=None, user=None, **context): + def serialize_contents(self, item, key, trans: ProvidesHistoryContext | None = None, user=None, **context): history = item returned = [] for content in self.manager.contents_manager._union_of_contents_query(history).all(): @@ -1005,7 +1035,7 @@ class HistorySerializer(sharable.SharableModelSerializer, deletable.PurgableSeri returned.append(serialized) return returned - def serialize_contents_states(self, item, key, trans=None, **context): + def serialize_contents_states(self, item, key, trans: ProvidesHistoryContext | None = None, **context): """ Return a dictionary containing the counts of all contents in each state keyed by the distinct states. @@ -1053,7 +1083,7 @@ class HistoryDeserializer(sharable.SharableModelDeserializer, deletable.Purgable def deserialize_preferred_object_store_id(self, item, key, val, **context): preferred_object_store_id = val validation_error = validate_preferred_object_store_id( - context["trans"], self.app.object_store, preferred_object_store_id + context["trans"].user, self.app.object_store, preferred_object_store_id ) if validation_error: raise ModelDeserializingError(validation_error) diff --git a/lib/galaxy/managers/history_contents.py b/lib/galaxy/managers/history_contents.py index 8dfbd15c20b..08b5185d6f0 100644 --- a/lib/galaxy/managers/history_contents.py +++ b/lib/galaxy/managers/history_contents.py @@ -45,6 +45,7 @@ from galaxy.managers import ( taggable, tools, ) +from galaxy.managers.context import ProvidesHistoryContext from galaxy.managers.job_connections import JobConnectionsManager from galaxy.model import batch_fetch_job_state_summaries from galaxy.schema import ValueFilterQueryParams @@ -242,7 +243,9 @@ class HistoryContentsManager(base.SortableManager): .filter_by(history_id=history_id) ) - def copy_contents(self, trans, history_id, payload: CopyDatasetsPayload) -> CopyDatasetsResponse: + def copy_contents( + self, trans: ProvidesHistoryContext, history_id, payload: CopyDatasetsPayload + ) -> CopyDatasetsResponse: user = trans.get_user() if not user: raise glx_exceptions.MessageException("Please login to copy datasets between histories.") diff --git a/lib/galaxy/managers/interactivetool.py b/lib/galaxy/managers/interactivetool.py index 22b1461a75f..7c34ba036fb 100644 --- a/lib/galaxy/managers/interactivetool.py +++ b/lib/galaxy/managers/interactivetool.py @@ -38,6 +38,7 @@ if TYPE_CHECKING: from galaxy.managers.context import ProvidesUserContext from galaxy.structured_app import MinimalManagerApp from galaxy.tools import Tool + from galaxy.webapps.base.webapp import GalaxyWebTransaction log = logging.getLogger(__name__) @@ -301,7 +302,7 @@ class InteractiveToolManager: self.sa_session.commit() self.propagator.remove_entry_point(entry_point) - def target_if_active(self, trans, entry_point: InteractiveToolEntryPoint) -> str | None: + def target_if_active(self, trans: "GalaxyWebTransaction", entry_point: InteractiveToolEntryPoint) -> str | None: if entry_point.active and not entry_point.deleted: use_it_proxy_host_cfg = ( not self.app.config.interactivetools_upstream_proxy and self.app.config.interactivetools_proxy_host @@ -354,7 +355,7 @@ class InteractiveToolManager: url_path += entry_point.entry_url.lstrip("/") return url_path - def access_entry_point_target(self, trans: "ProvidesUserContext", entry_point_id: int) -> str | None: + def access_entry_point_target(self, trans: "GalaxyWebTransaction", entry_point_id: int) -> str | None: entry_point = self.sa_session.get(InteractiveToolEntryPoint, entry_point_id) assert entry_point if self.can_access_entry_point(trans, entry_point): diff --git a/lib/galaxy/managers/item_tags.py b/lib/galaxy/managers/item_tags.py index 90f43e942b4..dd33477b324 100644 --- a/lib/galaxy/managers/item_tags.py +++ b/lib/galaxy/managers/item_tags.py @@ -3,7 +3,10 @@ from galaxy.exceptions import ( ObjectNotFound, ) from galaxy.managers import base -from galaxy.managers.context import ProvidesAppContext +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesUserContext, +) from galaxy.schema.fields import DecodedDatabaseIdField from galaxy.schema.item_tags import ItemTagsCreatePayload from galaxy.structured_app import MinimalManagerApp @@ -16,13 +19,13 @@ class ItemTagsManager: self._app = app self._tag_handler = app.tag_handler - def index(self, trans: ProvidesAppContext, tagged_item_class: str, tagged_item_id: DecodedDatabaseIdField): + def index(self, trans: ProvidesUserContext, tagged_item_class: str, tagged_item_id: DecodedDatabaseIdField): """Displays a collection (list) of tags associated with an item.""" tags = self._get_user_tags(trans, tagged_item_class, tagged_item_id) return [self._api_value(tag, trans, view="collection") for tag in tags] def show( - self, trans: ProvidesAppContext, tagged_item_class: str, tagged_item_id: DecodedDatabaseIdField, tag_name: str + self, trans: ProvidesUserContext, tagged_item_class: str, tagged_item_id: DecodedDatabaseIdField, tag_name: str ): """Displays information about a tag associated with an item.""" tag = self._get_item_tag_assoc(trans, tagged_item_class, tagged_item_id, tag_name) @@ -32,7 +35,7 @@ class ItemTagsManager: def create( self, - trans: ProvidesAppContext, + trans: ProvidesUserContext, tagged_item_class: str, tagged_item_id: DecodedDatabaseIdField, tag_name: str, @@ -44,7 +47,7 @@ class ItemTagsManager: return self._api_value(tag, trans) def delete( - self, trans: ProvidesAppContext, tagged_item_class: str, tagged_item_id: DecodedDatabaseIdField, tag_name: str + self, trans: ProvidesUserContext, tagged_item_class: str, tagged_item_id: DecodedDatabaseIdField, tag_name: str ): """Remove a tag from an item.""" deleted = self._remove_items_tag(trans, tagged_item_class, tagged_item_id, tag_name) @@ -52,35 +55,35 @@ class ItemTagsManager: raise NoContentException("Failed to delete specified tag.") return deleted - def _get_tagged_item(self, trans, item_class_name, id, check_ownership=True): + def _get_tagged_item(self, trans: ProvidesUserContext, item_class_name, id, check_ownership=True): tagged_item = base.get_object( trans, id, item_class_name, check_ownership=check_ownership, check_accessible=True ) return tagged_item - def _get_user_tags(self, trans, item_class_name, id): + def _get_user_tags(self, trans: ProvidesUserContext, item_class_name, id): user = trans.user tagged_item = self._get_tagged_item(trans, item_class_name, id) return [tag for tag in tagged_item.tags if tag.user == user] - def _remove_items_tag(self, trans, item_class_name, id, tag_name): + def _remove_items_tag(self, trans: ProvidesUserContext, item_class_name, id, tag_name): user = trans.user tagged_item = self._get_tagged_item(trans, item_class_name, id) deleted = tagged_item and self._tag_handler.remove_item_tag(user, tagged_item, tag_name) trans.sa_session.commit() return deleted - def _apply_item_tag(self, trans, item_class_name, id, tag_name, tag_value=None): + def _apply_item_tag(self, trans: ProvidesUserContext, item_class_name, id, tag_name, tag_value=None): user = trans.user tagged_item = self._get_tagged_item(trans, item_class_name, id) tag_assoc = self._tag_handler.apply_item_tag(user, tagged_item, tag_name, tag_value) trans.sa_session.commit() return tag_assoc - def _get_item_tag_assoc(self, trans, item_class_name, id, tag_name): + def _get_item_tag_assoc(self, trans: ProvidesUserContext, item_class_name, id, tag_name): user = trans.user tagged_item = self._get_tagged_item(trans, item_class_name, id) return self._tag_handler._get_item_tag_assoc(user, tagged_item, tag_name) - def _api_value(self, tag, trans, view="element"): + def _api_value(self, tag, trans: ProvidesAppContext, view="element"): return tag.to_dict(view=view, value_mapper={"id": trans.security.encode_id}) diff --git a/lib/galaxy/managers/jobs.py b/lib/galaxy/managers/jobs.py index e4d2f1c9edf..12780982b0e 100644 --- a/lib/galaxy/managers/jobs.py +++ b/lib/galaxy/managers/jobs.py @@ -50,7 +50,11 @@ from galaxy.job_metrics import ( Safety, ) from galaxy.managers.collections import DatasetCollectionManager -from galaxy.managers.context import ProvidesUserContext +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.managers.datasets import DatasetManager from galaxy.managers.hdas import ( dereference_input_to_hda, @@ -384,7 +388,7 @@ class JobManager: return job.history is not None and self.history_manager.is_accessible(job.history, user) def get_job_console_output( - self, trans, job, stdout_position=-1, stdout_length=0, stderr_position=-1, stderr_length=0 + self, trans: ProvidesAppContext, job, stdout_position=-1, stdout_length=0, stderr_position=-1, stderr_length=0 ): if job is None: raise ObjectNotFound() @@ -1517,7 +1521,7 @@ class JobSearch: return stmt -def view_show_job(trans, job: Job, full: bool) -> dict: +def view_show_job(trans: ProvidesUserContext, job: Job, full: bool) -> dict: is_admin = trans.user_is_admin job_dict = job.to_dict("element", system_details=is_admin) if trans.app.config.expose_dataset_path and "command_line" not in job_dict: @@ -1900,7 +1904,7 @@ def summarize_jobs_to_dict(sa_session, jobs_source) -> JobsSummary | None: return rval -def summarize_job_metrics(trans, job): +def summarize_job_metrics(trans: ProvidesUserContext, job): """Produce a dict-ified version of job metrics ready for tabular rendering. Precondition: the caller has verified the job is accessible to the user @@ -1927,7 +1931,7 @@ def summarize_metrics(trans: ProvidesUserContext, job_metrics): return [d.dict() for d in dictifiable_metrics] -def summarize_destination_params(trans, job): +def summarize_destination_params(trans: ProvidesUserContext, job): """Produce a dict-ified version of job destination parameters ready for tabular rendering. Precondition: the caller has verified the job is accessible to the user @@ -1944,7 +1948,7 @@ def summarize_destination_params(trans, job): return destination_params -def summarize_job_parameters(trans: ProvidesUserContext, job: Job) -> dict[str, Any]: +def summarize_job_parameters(trans: ProvidesHistoryContext, job: Job) -> dict[str, Any]: """Produce a dict-ified version of job parameters ready for tabular rendering. Precondition: the caller has verified the job is accessible to the user diff --git a/lib/galaxy/managers/landing.py b/lib/galaxy/managers/landing.py index e0d1b8e99da..f667c1719da 100644 --- a/lib/galaxy/managers/landing.py +++ b/lib/galaxy/managers/landing.py @@ -55,7 +55,10 @@ from galaxy.tool_util_models.parameters import ( ToolParameterBundleModel, ) from galaxy.util import safe_str_cmp -from .context import ProvidesUserContext +from .context import ( + ProvidesHistoryContext, + ProvidesUserContext, +) from .headers_encryption import ( decrypt_headers_in_data, encrypt_headers_in_data, @@ -237,7 +240,7 @@ class LandingRequestManager: return self._tool_response(request) def claim_workflow_landing_request( - self, trans: ProvidesUserContext, uuid: UUID4, claim: ClaimLandingPayload | None + self, trans: ProvidesHistoryContext, uuid: UUID4, claim: ClaimLandingPayload | None ) -> WorkflowLandingRequest: request = self._get_workflow_landing_request(uuid) self._check_can_claim(trans, request, claim) @@ -246,7 +249,7 @@ class LandingRequestManager: self._save(request) return self._workflow_response(request) - def _ensure_workflow(self, trans: ProvidesUserContext, request: WorkflowLandingRequestModel): + def _ensure_workflow(self, trans: ProvidesHistoryContext, request: WorkflowLandingRequestModel): if request.workflow_source_type == "trs_url" and isinstance(trans.app, StructuredApp): # trans is always structured app except for unit test assert request.workflow_source @@ -266,7 +269,7 @@ class LandingRequestManager: request = self._get_claimed_tool_landing_request(trans, uuid) return self._tool_response(request) - def get_workflow_landing_request(self, trans: ProvidesUserContext, uuid: UUID4) -> WorkflowLandingRequest: + def get_workflow_landing_request(self, trans: ProvidesHistoryContext, uuid: UUID4) -> WorkflowLandingRequest: request = self._get_claimed_workflow_landing_request(trans, uuid) self._ensure_workflow(trans, request) return self._workflow_response(request) diff --git a/lib/galaxy/managers/lddas.py b/lib/galaxy/managers/lddas.py index 32e698ad4f9..6d4b188eed9 100644 --- a/lib/galaxy/managers/lddas.py +++ b/lib/galaxy/managers/lddas.py @@ -4,6 +4,7 @@ from typing import ( ) from galaxy.managers import base as manager_base +from galaxy.managers.context import ProvidesUserContext from galaxy.managers.datasets import DatasetAssociationManager from galaxy.model import ( LibraryDatasetDatasetAssociation, @@ -27,7 +28,7 @@ class LDDAManager(DatasetAssociationManager[LibraryDatasetDatasetAssociation]): """ super().__init__(app) - def get(self, trans, id: int, check_accessible=True) -> LibraryDatasetDatasetAssociation: + def get(self, trans: ProvidesUserContext, id: int, check_accessible=True) -> LibraryDatasetDatasetAssociation: return manager_base.get_object( trans, id, "LibraryDatasetDatasetAssociation", check_ownership=False, check_accessible=check_accessible ) @@ -41,7 +42,7 @@ class LDDAManager(DatasetAssociationManager[LibraryDatasetDatasetAssociation]): return True return item.user == user - def _set_permissions(self, trans, library_dataset, role_ids_dict): + def _set_permissions(self, trans: ProvidesUserContext, library_dataset, role_ids_dict): # Check Git history for an older broken implementation, but it was broken # and security related and had not test coverage so it was deleted. raise NotImplementedError() diff --git a/lib/galaxy/managers/libraries.py b/lib/galaxy/managers/libraries.py index 584272dd494..96999b7f2ae 100644 --- a/lib/galaxy/managers/libraries.py +++ b/lib/galaxy/managers/libraries.py @@ -11,6 +11,10 @@ from sqlalchemy.exc import ( from sqlalchemy.orm import Query from galaxy import exceptions +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesUserContext, +) from galaxy.managers.folders import FolderManager from galaxy.model import ( Library, @@ -42,7 +46,7 @@ class LibraryManager: Interface/service object for interacting with libraries. """ - def get(self, trans, decoded_library_id: int, check_accessible: bool = True) -> Library: + def get(self, trans: ProvidesUserContext, decoded_library_id: int, check_accessible: bool = True) -> Library: """ Get the library from the DB. @@ -65,7 +69,9 @@ class LibraryManager: library = self.secure(trans, library, check_accessible) return library - def create(self, trans, name: str, description: str | None = "", synopsis: str | None = "") -> Library: + def create( + self, trans: ProvidesUserContext, name: str, description: str | None = "", synopsis: str | None = "" + ) -> Library: """ Create a new library. """ @@ -81,7 +87,7 @@ class LibraryManager: def update( self, - trans, + trans: ProvidesUserContext, library: Library, name: str | None = None, description: str | None = None, @@ -116,7 +122,7 @@ class LibraryManager: trans.sa_session.commit() return library - def delete(self, trans, library: Library, undelete: bool | None = False) -> Library: + def delete(self, trans: ProvidesUserContext, library: Library, undelete: bool | None = False) -> Library: """ Mark given library deleted/undeleted based on the flag. """ @@ -130,7 +136,7 @@ class LibraryManager: trans.sa_session.commit() return library - def list(self, trans, deleted: bool | None = False) -> tuple[Query, dict[str, set]]: + def list(self, trans: ProvidesUserContext, deleted: bool | None = False) -> tuple[Query, dict[str, set]]: """ Return a list of libraries from the DB. @@ -182,7 +188,7 @@ class LibraryManager: return libraries, prefetched_ids - def secure(self, trans, library: Library, check_accessible: bool = True) -> Library: + def secure(self, trans: ProvidesUserContext, library: Library, check_accessible: bool = True) -> Library: """ Check if library is accessible to user. @@ -201,7 +207,7 @@ class LibraryManager: library = self.check_accessible(trans, library) return library - def check_accessible(self, trans, library: Library) -> Library: + def check_accessible(self, trans: ProvidesUserContext, library: Library) -> Library: """ Check whether the library is accessible to current user. """ @@ -212,7 +218,9 @@ class LibraryManager: else: return library - def get_library_dict(self, trans, library: Library, prefetched_ids: dict[str, set] | None = None) -> dict: + def get_library_dict( + self, trans: ProvidesUserContext, library: Library, prefetched_ids: dict[str, set] | None = None + ) -> dict: """ Return library data in the form of a dictionary. @@ -262,7 +270,7 @@ class LibraryManager: library_dict["can_user_manage"] = True return library_dict - def get_current_roles(self, trans, library: Library) -> dict: + def get_current_roles(self, trans: ProvidesAppContext, library: Library) -> dict: """ Load all permissions currently related to the given library. @@ -286,13 +294,13 @@ class LibraryManager: add_library_item_role_list=role_name_id_pairs(add_roles, private_role_emails, encode_id), ) - def get_access_roles(self, trans, library: Library) -> set[Role]: + def get_access_roles(self, trans: ProvidesAppContext, library: Library) -> set[Role]: """ Load access roles for all library permissions """ return set(library.get_access_roles(trans.app.security_agent)) - def get_modify_roles(self, trans, library: Library) -> set[Role]: + def get_modify_roles(self, trans: ProvidesAppContext, library: Library) -> set[Role]: """ Load modify roles for all library permissions """ @@ -302,7 +310,7 @@ class LibraryManager: ) ) - def get_manage_roles(self, trans, library: Library) -> set[Role]: + def get_manage_roles(self, trans: ProvidesAppContext, library: Library) -> set[Role]: """ Load manage roles for all library permissions """ @@ -312,7 +320,7 @@ class LibraryManager: ) ) - def get_add_roles(self, trans, library: Library) -> set[Role]: + def get_add_roles(self, trans: ProvidesAppContext, library: Library) -> set[Role]: """ Load add roles for all library permissions """ @@ -322,21 +330,21 @@ class LibraryManager: ) ) - def make_public(self, trans, library: Library) -> bool: + def make_public(self, trans: ProvidesAppContext, library: Library) -> bool: """ Makes the given library public (removes all access roles) """ trans.app.security_agent.make_library_public(library) return self.is_public(trans, library) - def is_public(self, trans, library: Library) -> bool: + def is_public(self, trans: ProvidesAppContext, library: Library) -> bool: """ Return true if lib is public. """ return trans.app.security_agent.library_is_public(library) -def get_containing_library_from_library_dataset(trans, library_dataset) -> Library | None: +def get_containing_library_from_library_dataset(trans: ProvidesAppContext, library_dataset) -> Library | None: """Given a library_dataset, get the containing library""" folder = library_dataset.folder while folder.parent: diff --git a/lib/galaxy/managers/library_datasets.py b/lib/galaxy/managers/library_datasets.py index 068a0ae03c7..425f8b0a374 100644 --- a/lib/galaxy/managers/library_datasets.py +++ b/lib/galaxy/managers/library_datasets.py @@ -15,7 +15,11 @@ from galaxy.exceptions import ( RequestParameterInvalidException, ) from galaxy.managers.base import ModelManager -from galaxy.managers.context import ProvidesUserContext +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.managers.lddas import LDDAManager from galaxy.model import ( LibraryDataset, @@ -37,7 +41,7 @@ class LibraryDatasetsManager(ModelManager[LibraryDataset]): super().__init__(app) self.ldda_manager = LDDAManager(app) - def get(self, trans, decoded_library_dataset_id, check_accessible=True) -> LibraryDataset: + def get(self, trans: ProvidesUserContext, decoded_library_dataset_id, check_accessible=True) -> LibraryDataset: """ Get the library dataset from the DB. @@ -90,7 +94,7 @@ class LibraryDatasetsManager(ModelManager[LibraryDataset]): def _set_from_dict( self, - trans: ProvidesUserContext, + trans: ProvidesHistoryContext, ldda: LibraryDatasetDatasetAssociation, new_data: dict[str, Any], flush: bool = True, @@ -166,7 +170,7 @@ class LibraryDatasetsManager(ModelManager[LibraryDataset]): validated_payload[key] = val return validated_payload - def secure(self, trans, ld, check_accessible=True, check_ownership=False): + def secure(self, trans: ProvidesUserContext, ld, check_accessible=True, check_ownership=False): """ Check if library dataset is accessible to current user or the user is an admin. @@ -185,7 +189,7 @@ class LibraryDatasetsManager(ModelManager[LibraryDataset]): ld = self.check_accessible(trans, ld) return ld - def check_accessible(self, trans, ld): + def check_accessible(self, trans: ProvidesUserContext, ld): """ Check whether the current user has permissions to access library dataset. @@ -204,7 +208,7 @@ class LibraryDatasetsManager(ModelManager[LibraryDataset]): else: return ld - def check_modifiable(self, trans, ld): + def check_modifiable(self, trans: ProvidesUserContext, ld): """ Check whether the current user has permissions to modify library dataset. @@ -225,7 +229,7 @@ class LibraryDatasetsManager(ModelManager[LibraryDataset]): else: return ld - def serialize(self, trans, ld: LibraryDataset) -> dict[str, Any]: + def serialize(self, trans: ProvidesUserContext, ld: LibraryDataset) -> dict[str, Any]: """Serialize the library dataset into a dictionary.""" current_user_roles = trans.get_current_user_roles() @@ -271,7 +275,7 @@ class LibraryDatasetsManager(ModelManager[LibraryDataset]): ) return rval - def _build_path(self, trans, folder): + def _build_path(self, trans: ProvidesAppContext, folder): """ Search the path upwards recursively and load the whole route of names and ids for breadcrumb building purposes. diff --git a/lib/galaxy/managers/markdown_util.py b/lib/galaxy/managers/markdown_util.py index ec3704f522c..7a07211d54e 100644 --- a/lib/galaxy/managers/markdown_util.py +++ b/lib/galaxy/managers/markdown_util.py @@ -20,9 +20,7 @@ import shutil import tempfile from datetime import datetime from re import Match -from typing import ( - Any, -) +from typing import Any import markdown @@ -37,6 +35,11 @@ from galaxy.exceptions import ( ObjectNotFound, ServerNotConfiguredForRequest, ) +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.managers.jobs import ( JobManager, summarize_job_metrics, @@ -107,7 +110,7 @@ def process_invocation_ids(f, workflow_markdown: str) -> str: return re.sub(VISUALIZATION_FENCED_BLOCK, process_block, workflow_markdown) -def ready_galaxy_markdown_for_import(trans, external_galaxy_markdown): +def ready_galaxy_markdown_for_import(trans: ProvidesAppContext, external_galaxy_markdown): """Convert from encoded IDs to decoded numeric IDs for storing in the DB.""" _validate(external_galaxy_markdown, internal=False) @@ -138,11 +141,12 @@ def ready_galaxy_markdown_for_import(trans, external_galaxy_markdown): class GalaxyInternalMarkdownDirectiveHandler(metaclass=abc.ABCMeta): - def walk(self, trans, internal_galaxy_markdown): + def walk(self, trans: ProvidesHistoryContext, internal_galaxy_markdown): hda_manager = trans.app.hda_manager history_manager = trans.app.history_manager workflow_manager = trans.app.workflow_manager - job_manager = JobManager(trans.app, history_manager) + # not trans.app.job_manager, which is the job queue manager of the same name + job_manager = trans.app[JobManager] collection_manager = trans.app.dataset_collection_manager def _job_for_job_directive(object_type, object_id): @@ -325,10 +329,12 @@ class GalaxyInternalMarkdownDirectiveHandler(metaclass=abc.ABCMeta): if container == "history_dataset_type": _check_object(object_id, match.group(0)) + assert object_id is not None hda = hda_manager.get_accessible(object_id, trans.user) return hda.extension or "data" elif container == "history_dataset_name": _check_object(object_id, match.group(0)) + assert object_id is not None hda = hda_manager.get_accessible(object_id, trans.user) return hda.name or "" elif container == "workflow_license": @@ -337,6 +343,7 @@ class GalaxyInternalMarkdownDirectiveHandler(metaclass=abc.ABCMeta): return _workflow_license_as_simple_markdown(stored_workflow) elif container == "invocation_time": _check_object(object_id, match.group(0)) + assert object_id is not None invocation = workflow_manager.get_invocation(trans, object_id) return _database_time_to_str(invocation.create_time) elif container == "generate_time": @@ -368,6 +375,7 @@ class GalaxyInternalMarkdownDirectiveHandler(metaclass=abc.ABCMeta): return _link_to_markdown(url, title) elif container == "history_dataset_as_image": _check_object(object_id, match.group(0)) + assert object_id is not None hda = hda_manager.get_accessible(object_id, trans.user) return f"![{hda.name}](gxdatasetasimage://{encoded_id})" else: @@ -394,7 +402,7 @@ class GalaxyInternalMarkdownDirectiveHandler(metaclass=abc.ABCMeta): ) return export_markdown, export_markdown_embed_expanded - def _encode_line(self, trans, line): + def _encode_line(self, trans: ProvidesAppContext, line): object_type = None object_id = None encoded_id = None @@ -531,7 +539,7 @@ class GalaxyInternalMarkdownDirectiveHandler(metaclass=abc.ABCMeta): class ReadyForExportMarkdownDirectiveHandler(GalaxyInternalMarkdownDirectiveHandler): - def __init__(self, trans, extra_rendering_data=None): + def __init__(self, trans: ProvidesHistoryContext, extra_rendering_data=None): extra_rendering_data = extra_rendering_data if extra_rendering_data is not None else {} self.trans = trans self.extra_rendering_data = extra_rendering_data @@ -646,7 +654,7 @@ class ReadyForExportMarkdownDirectiveHandler(GalaxyInternalMarkdownDirectiveHand return (line, False) -def ready_galaxy_markdown_for_export(trans, internal_galaxy_markdown): +def ready_galaxy_markdown_for_export(trans: ProvidesHistoryContext, internal_galaxy_markdown): """Fill in details needed to render Galaxy flavored markdown. Take it from a minimal internal version to an externally render-able version @@ -668,7 +676,7 @@ def ready_galaxy_markdown_for_export(trans, internal_galaxy_markdown): class ToBasicMarkdownDirectiveHandler(GalaxyInternalMarkdownDirectiveHandler): - def __init__(self, trans): + def __init__(self, trans: ProvidesHistoryContext): self.trans = trans def _format_printable_time(self, time): @@ -918,7 +926,7 @@ class ToBasicMarkdownDirectiveHandler(GalaxyInternalMarkdownDirectiveHandler): return (line, False) -def to_basic_markdown(trans, internal_galaxy_markdown: str) -> str: +def to_basic_markdown(trans: ProvidesHistoryContext, internal_galaxy_markdown: str) -> str: """Replace Galaxy Markdown extensions with plain Markdown for PDF/HTML export.""" directive_handler = ToBasicMarkdownDirectiveHandler(trans) resolved_invocations_markdown = resolve_invocation_markdown(trans, internal_galaxy_markdown) @@ -964,7 +972,9 @@ def _check_can_convert_to_pdf_or_raise(): raise ServerNotConfiguredForRequest("PDF conversion service not available.") -def internal_galaxy_markdown_to_pdf(trans, internal_galaxy_markdown: str, document_type: PdfDocumentType) -> bytes: +def internal_galaxy_markdown_to_pdf( + trans: ProvidesHistoryContext, internal_galaxy_markdown: str, document_type: PdfDocumentType +) -> bytes: _check_can_convert_to_pdf_or_raise() basic_markdown = to_basic_markdown(trans, internal_galaxy_markdown) config = trans.app.config @@ -1002,7 +1012,7 @@ def to_branded_pdf(basic_markdown: str, document_type: PdfDocumentType, config: return to_pdf_raw(branded_markdown, css_paths=css_paths) -def populate_invocation_markdown(trans, invocation, workflow_markdown): +def populate_invocation_markdown(trans: ProvidesHistoryContext, invocation, workflow_markdown): """ Resolve invocation objects to convert markdown to 'internal' representation. @@ -1074,7 +1084,7 @@ def populate_invocation_markdown(trans, invocation, workflow_markdown): return galaxy_markdown -def resolve_invocation_markdown(trans, workflow_markdown): +def resolve_invocation_markdown(trans: ProvidesUserContext, workflow_markdown): """Resolve invocation objects to convert markdown to 'internal' representation. Replace references to abstract workflow parts with actual galaxy object IDs corresponding @@ -1089,7 +1099,7 @@ def resolve_invocation_markdown(trans, workflow_markdown): Hopefully this list will be expanded to include invocation_qc and step_output. """ - def get_invocation(trans, line): + def get_invocation(trans: ProvidesUserContext, line): workflow_manager = trans.app.workflow_manager if invocation_id_match := re.search(INVOCATION_ID_PATTERN, line): invocation_id = int(invocation_id_match.group(1)) @@ -1249,7 +1259,7 @@ def resolve_invocation_markdown(trans, workflow_markdown): return workflow_markdown -def resolve_job_markdown(trans, job, job_markdown): +def resolve_job_markdown(trans: ProvidesHistoryContext, job, job_markdown): """Resolve job objects to convert tool markdown to 'internal' representation. Replace references to abstract workflow parts with actual galaxy object IDs corresponding diff --git a/lib/galaxy/managers/object_store_instances.py b/lib/galaxy/managers/object_store_instances.py index f4b7f0c68e0..c0e795b628a 100644 --- a/lib/galaxy/managers/object_store_instances.py +++ b/lib/galaxy/managers/object_store_instances.py @@ -332,7 +332,9 @@ class ObjectStoreInstancesManager: template = catalog.find_template_by(persisted_object_store.template_id, target_template_version) return template - def _to_model(self, trans, persisted_object_store: UserObjectStore) -> UserConcreteObjectStoreModel: + def _to_model( + self, trans: ProvidesUserContext, persisted_object_store: UserObjectStore + ) -> UserConcreteObjectStoreModel: quota = QuotaModel(source=None, enabled=False) object_store_type = persisted_object_store.template.configuration.type admin_badges = persisted_object_store.template.configuration.badges or [] diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index 1e76e6f10a7..fe10cb2b3ff 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -34,6 +34,7 @@ from galaxy.managers import ( sharable, ) from galaxy.managers.context import ( + ProvidesAppContext, ProvidesHistoryContext, ProvidesUserContext, ) @@ -269,7 +270,7 @@ class PageManager(sharable.SharableModelManager[model.Page], UsesAnnotations): stmt = stmt.offset(payload.offset) return trans.sa_session.scalars(stmt), total_matches - def create_page(self, trans, payload: CreatePagePayload): + def create_page(self, trans: ProvidesUserContext, payload: CreatePagePayload): user = trans.get_user() if not user: raise exceptions.AuthenticationRequired("You must be logged in to create pages.") @@ -341,7 +342,7 @@ class PageManager(sharable.SharableModelManager[model.Page], UsesAnnotations): session.commit() return page - def update_page(self, trans, id: int, payload: UpdatePagePayload): + def update_page(self, trans: ProvidesUserContext, id: int, payload: UpdatePagePayload): user = trans.get_user() if not user: raise exceptions.AuthenticationRequired("You must be logged in to update pages.") @@ -386,7 +387,7 @@ class PageManager(sharable.SharableModelManager[model.Page], UsesAnnotations): session.commit() return page - def save_new_revision(self, trans, page, payload): + def save_new_revision(self, trans: ProvidesAppContext, page, payload): # Assumes security has already been checked by caller. content = payload.get("content", None) content_format = payload.get("content_format", None) @@ -420,18 +421,18 @@ class PageManager(sharable.SharableModelManager[model.Page], UsesAnnotations): session.commit() return page_revision - def list_revisions(self, trans, page, sort_desc: bool = False): + def list_revisions(self, trans: ProvidesUserContext, page, sort_desc: bool = False): page = base.security_check(trans, page, check_ownership=False, check_accessible=True) return sorted(page.revisions, key=lambda r: r.create_time, reverse=sort_desc) - def get_revision(self, trans, page, revision_id): + def get_revision(self, trans: ProvidesUserContext, page, revision_id): page = base.security_check(trans, page, check_ownership=False, check_accessible=True) revision = trans.sa_session.get(model.PageRevision, revision_id) if not revision or revision.page_id != page.id: raise exceptions.ObjectNotFound("Page revision not found") return revision - def restore_revision(self, trans, page, revision_id): + def restore_revision(self, trans: ProvidesUserContext, page, revision_id): page = base.security_check(trans, page, check_ownership=True, check_accessible=True) old_revision = self.get_revision(trans, page, revision_id) # Build revision directly — content is already in internal format @@ -446,7 +447,7 @@ class PageManager(sharable.SharableModelManager[model.Page], UsesAnnotations): trans.sa_session.commit() return page_revision - def rewrite_content_for_import(self, trans, content, content_format: str): + def rewrite_content_for_import(self, trans: ProvidesAppContext, content, content_format: str): if content_format == PageContentFormat.html.value: try: content = sanitize_html(content) @@ -466,7 +467,7 @@ class PageManager(sharable.SharableModelManager[model.Page], UsesAnnotations): ) return content - def rewrite_content_for_export(self, trans, as_dict): + def rewrite_content_for_export(self, trans: ProvidesHistoryContext, as_dict): content = as_dict["content"] content_format = as_dict.get("content_format", PageContentFormat.html.value) if content_format == PageContentFormat.html.value: @@ -554,7 +555,7 @@ class PageContentProcessor(HTMLParser): "wbr", } - def __init__(self, trans, render_embed_html_fn: Callable): + def __init__(self, trans: ProvidesAppContext, render_embed_html_fn: Callable): HTMLParser.__init__(self) self.trans = trans self.ignore_content = False diff --git a/lib/galaxy/managers/quotas.py b/lib/galaxy/managers/quotas.py index fa1e486d275..8e1bff8bc6f 100644 --- a/lib/galaxy/managers/quotas.py +++ b/lib/galaxy/managers/quotas.py @@ -20,6 +20,7 @@ from galaxy import ( ) from galaxy.exceptions import ActionInputError from galaxy.managers import base +from galaxy.managers.context import ProvidesUserContext from galaxy.model import ( Group, Quota, @@ -275,5 +276,5 @@ class QuotaManager: message += ", ".join(names) return message - def get_quota(self, trans, id: int, deleted: bool | None = None) -> model.Quota: + def get_quota(self, trans: ProvidesUserContext, id: int, deleted: bool | None = None) -> model.Quota: return base.get_object(trans, id, "Quota", check_ownership=False, check_accessible=False, deleted=deleted) diff --git a/lib/galaxy/managers/sharable.py b/lib/galaxy/managers/sharable.py index acf9fd48dca..9f1bf5b2daf 100644 --- a/lib/galaxy/managers/sharable.py +++ b/lib/galaxy/managers/sharable.py @@ -37,6 +37,7 @@ from galaxy.managers import ( users, ) from galaxy.managers.base import combine_lists +from galaxy.managers.context import ProvidesUserContext from galaxy.model import ( User, UserShareAssociation, @@ -244,7 +245,7 @@ class SharableModelManager( return list(self._apply_fn_limit_offset_gen(items, limit, offset)) def get_sharing_extra_information( - self, trans, item, users: set[User], errors: set[str], option: SharingOptions | None = None + self, trans: ProvidesUserContext, item, users: set[User], errors: set[str], option: SharingOptions | None = None ) -> ShareWithExtra | None: """Returns optional extra information about the shareability of the given item. @@ -252,7 +253,7 @@ class SharableModelManager( to provide the extra information, otherwise, it will be None by default.""" return None - def make_members_public(self, trans, item): + def make_members_public(self, trans: ProvidesUserContext, item): """Make potential elements of this item public. This method must be overridden in managers that need to change permissions of internal elements diff --git a/lib/galaxy/managers/tool_data.py b/lib/galaxy/managers/tool_data.py index b46e522e886..441450d17dc 100644 --- a/lib/galaxy/managers/tool_data.py +++ b/lib/galaxy/managers/tool_data.py @@ -72,7 +72,7 @@ class ToolDataManager: data_table.reload_from_files() return self._reload_data_table(table_name) - def get_field_file_path(self, trans, table_name: str, field_name: str, file_name: str) -> Path: + def get_field_file_path(self, trans: ProvidesUserContext, table_name: str, field_name: str, file_name: str) -> Path: """Get the absolute path to a given file name in the table field""" field_value = self._data_table_field(table_name, field_name) if table_name not in PUBLIC_TABLES and not trans.user_is_admin: diff --git a/lib/galaxy/managers/tours.py b/lib/galaxy/managers/tours.py index 3542fafca40..b64477843ab 100644 --- a/lib/galaxy/managers/tours.py +++ b/lib/galaxy/managers/tours.py @@ -9,7 +9,7 @@ from galaxy.exceptions import ( ObjectNotFound, RequestParameterInvalidException, ) -from galaxy.managers.context import ProvidesAppContext +from galaxy.managers.context import ProvidesHistoryContext from galaxy.schema.schema import GenerateTourResponse from galaxy.schema.tours import ( TourDetails, @@ -27,7 +27,7 @@ class ToursManager: self._app = app def generate_tour( - self, tool_id: str, tool_version: str, trans: ProvidesAppContext, performs_upload=True + self, tool_id: str, tool_version: str, trans: ProvidesHistoryContext, performs_upload=True ) -> GenerateTourResponse: """ Generate a tour designed for the given tool. @@ -51,7 +51,7 @@ class ToursManager: class TourGenerator: - def __init__(self, trans: ProvidesAppContext, tool_id: str, tool_version: str, performs_upload=True) -> None: + def __init__(self, trans: ProvidesHistoryContext, tool_id: str, tool_version: str, performs_upload=True) -> None: self._trans = trans self._tool: Tool = self._get_and_ensure_tool(tool_id, tool_version) self._use_datasets = True diff --git a/lib/galaxy/managers/users.py b/lib/galaxy/managers/users.py index f914e000051..a6b35ce81b5 100644 --- a/lib/galaxy/managers/users.py +++ b/lib/galaxy/managers/users.py @@ -9,6 +9,7 @@ import string import time from typing import ( Any, + TYPE_CHECKING, ) from markupsafe import escape @@ -32,6 +33,11 @@ from galaxy.managers import ( deletable, ) from galaxy.managers.base import combine_lists +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.model import ( Job, User, @@ -57,6 +63,9 @@ from galaxy.structured_app import ( from galaxy.util import now from galaxy.util.hash_util import new_secure_hash_v2 +if TYPE_CHECKING: + from galaxy.webapps.base.webapp import GalaxyWebTransaction + log = logging.getLogger(__name__) PASSWORD_RESET_TEMPLATE = """ @@ -88,7 +97,9 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): self.app_type = app_type super().__init__(app) - def register(self, trans, email=None, username=None, password=None, confirm=None, subscribe=False): + def register( + self, trans: "GalaxyWebTransaction", email=None, username=None, password=None, confirm=None, subscribe=False + ): """ Register a new user. """ @@ -164,7 +175,13 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): return user def update_email( - self, trans, user: User, new_email: str, *, commit: bool = True, send_activation_email: bool = True + self, + trans: ProvidesAppContext, + user: User, + new_email: str, + *, + commit: bool = True, + send_activation_email: bool = True, ) -> None: """ Update a user's email address, keeping the private role in sync and honoring activation settings. @@ -192,7 +209,7 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): if commit: session.commit() - def update_username(self, trans, user: User, new_username: str, *, commit: bool = True) -> None: + def update_username(self, trans: ProvidesAppContext, user: User, new_username: str, *, commit: bool = True) -> None: """ Update a user's public name after validating it. Raises RequestParameterInvalidException on validation errors. """ @@ -378,7 +395,7 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): return util.safe_str_cmp(bootstrap_hash, provided_hash) # ---- admin - def is_admin(self, user: model.User | None, trans=None) -> bool: + def is_admin(self, user: model.User | None, trans: ProvidesUserContext | None = None) -> bool: """Return True if this user is an admin (or session is authenticated as admin). Do not pass trans to simply check if an existing user object is an admin user, @@ -387,7 +404,7 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): if user is None: # Anonymous session or master_api_key used, if master_api_key is detected # return True. - return trans and trans.user_is_admin + return bool(trans and trans.user_is_admin) return self.app.config.is_admin_user(user) def admins(self, filters=None, **kwargs): @@ -440,7 +457,7 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): return user # ---- current - def current_user(self, trans): + def current_user(self, trans: ProvidesUserContext): # define here for single point of change and make more readable # TODO: trans return trans.user @@ -508,7 +525,7 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): else: return user, "User not found." - def __set_password(self, trans, user, password, confirm): + def __set_password(self, trans: ProvidesUserContext, user, password, confirm): if not password: return "Please provide a new password." if user: @@ -537,14 +554,14 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): else: return "Failed to determine user, access denied." - def impersonate(self, trans, user): + def impersonate(self, trans: "GalaxyWebTransaction", user): if not trans.app.config.allow_user_impersonation: - raise exceptions.Message("User impersonation is not enabled in this instance of Galaxy.") + raise exceptions.MessageException("User impersonation is not enabled in this instance of Galaxy.") if user: trans.handle_user_logout() trans.handle_user_login(user) else: - raise exceptions.Message("Please provide a valid user.") + raise exceptions.MessageException("Please provide a valid user.") def send_activation_email(self, trans, email, username): """ @@ -582,7 +599,7 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): log.exception("Unable to send the activation email.") return False - def __get_activation_token(self, trans, email): + def __get_activation_token(self, trans: ProvidesAppContext, email): """ Check for the activation token. Create new activation token and store it in the database if no token found. Flushes but does not commit—the caller is responsible for committing the transaction. @@ -591,6 +608,7 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): # Flush pending changes so the user is visible to the DB query below. session.flush() user = get_user_by_email(session, email, self.app.model.User) + assert user is not None, f"User with email '{email}' not found while generating activation token." activation_token = user.activation_token if activation_token is None: activation_token = util.hash_util.new_secure_hash_v2(str(random.getrandbits(256))) @@ -599,7 +617,7 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): session.flush() return activation_token - def send_reset_email(self, trans, payload, **kwd): + def send_reset_email(self, trans: "GalaxyWebTransaction", payload, **kwd): """Reset the user's password. Send an email with token that allows a password change.""" if self.app.config.smtp_server is None: return "Mail is not configured for this Galaxy instance and password reset information cannot be sent. Please contact your local Galaxy administrator." @@ -632,7 +650,7 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): log.warning(f"Failed to produce password reset token. User with email '{email}' not found.") return None - def get_reset_token(self, trans, email): + def get_reset_token(self, trans: ProvidesAppContext, email): reset_user = self.by_email(email) if not reset_user: reset_user = self.by_email(email, case_sensitive=False) @@ -799,17 +817,21 @@ class UserDeserializer(base.ModelDeserializer): } self.deserializers.update(user_deserializers) - def deserialize_preferred_object_store_id(self, item: Any, key: Any, val: Any, trans=None, **context): + def deserialize_preferred_object_store_id( + self, item: Any, key: Any, val: Any, trans: ProvidesUserContext | None = None, **context + ): preferred_object_store_id = val - validation_error = validate_preferred_object_store_id(trans, self.app.object_store, preferred_object_store_id) + validation_error = validate_preferred_object_store_id( + trans.user if trans else None, self.app.object_store, preferred_object_store_id + ) if validation_error: raise base.ModelDeserializingError(validation_error) return self.default_deserializer(item, key, preferred_object_store_id, **context) - def deserialize_username(self, item, key, username, trans=None, **context): + def deserialize_username(self, item, key, username, trans: ProvidesAppContext | None = None, **context): # TODO: validate_publicname requires trans and should(?) raise exceptions # move validation to UserValidator and use self.app, exceptions instead - validation_error = validate_publicname(trans, username, user=item) + validation_error = validate_publicname(trans, username, user=item) # type: ignore[arg-type] if validation_error: raise base.ModelDeserializingError(validation_error) return self.default_deserializer(item, key, username, trans=trans, **context) @@ -827,14 +849,14 @@ class CurrentUserSerializer(UserSerializer): return self.serialize_current_anonymous_user(user, keys, **kwargs) return super(UserSerializer, self).serialize(user, keys, **kwargs) - def serialize_current_anonymous_user(self, user, keys, trans=None, **kwargs): + def serialize_current_anonymous_user(self, user, keys, trans: ProvidesHistoryContext | None = None, **kwargs): # use the current history if any to get usage stats for trans' anonymous user # TODO: might be better as sep. Serializer class - usage = 0 + usage: float = 0 percent = None - if hasattr(trans, "history") and trans.history: - usage = self.app.quota_agent.get_usage(trans, history=trans.history) + if trans is not None and trans.history: + usage = self.app.quota_agent.get_usage(trans, history=trans.history) or 0 percent = self.app.quota_agent.get_percent(trans=trans, usage=usage) # a very small subset of keys available diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index 85e6d0ab48e..3972a429d88 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -57,7 +57,11 @@ from galaxy.managers.base import ( decode_id, security_check, ) -from galaxy.managers.context import ProvidesUserContext +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.managers.executables import artifact_class from galaxy.model import ( History, @@ -296,7 +300,7 @@ class WorkflowsManager(sharable.SharableModelManager[model.StoredWorkflow], dele result = trans.sa_session.scalars(stmt).unique() return result, total_matches - def get_stored_workflow(self, trans, workflow_id, by_stored_id=True) -> StoredWorkflow: + def get_stored_workflow(self, trans: ProvidesUserContext, workflow_id, by_stored_id=True) -> StoredWorkflow: """Use a supplied ID (UUID or encoded stored workflow ID) to find a workflow. """ @@ -318,7 +322,7 @@ class WorkflowsManager(sharable.SharableModelManager[model.StoredWorkflow], dele raise exceptions.ObjectNotFound("No such workflow found.") return stored_workflow - def get_stored_accessible_workflow(self, trans, workflow_id, by_stored_id=True): + def get_stored_accessible_workflow(self, trans: ProvidesUserContext, workflow_id, by_stored_id=True): """Get a stored workflow from a encoded stored workflow id and make sure it accessible to the user. """ @@ -335,7 +339,7 @@ class WorkflowsManager(sharable.SharableModelManager[model.StoredWorkflow], dele return stored_workflow - def attach_stored_workflow(self, trans, workflow): + def attach_stored_workflow(self, trans: ProvidesUserContext, workflow): """Attach and return stored workflow if possible.""" # Imported Subworkflows are not created with a StoredWorkflow association # To properly serialize them we do need a StoredWorkflow, so we create and attach one here. @@ -346,7 +350,7 @@ class WorkflowsManager(sharable.SharableModelManager[model.StoredWorkflow], dele trans.sa_session.commit() return stored_workflow - def get_owned_workflow(self, trans, encoded_workflow_id): + def get_owned_workflow(self, trans: ProvidesUserContext, encoded_workflow_id): """Get a workflow (non-stored) from a encoded workflow id and make sure it accessible to the user. """ @@ -355,7 +359,7 @@ class WorkflowsManager(sharable.SharableModelManager[model.StoredWorkflow], dele self.check_security(trans, workflow, check_ownership=True) return workflow - def check_security(self, trans, has_workflow, check_ownership=True, check_accessible=True): + def check_security(self, trans: ProvidesUserContext, has_workflow, check_ownership=True, check_accessible=True): """check accessibility or ownership of workflows, storedworkflows, and workflowinvocations. Throw an exception or returns True if user has needed level of access. @@ -391,12 +395,12 @@ class WorkflowsManager(sharable.SharableModelManager[model.StoredWorkflow], dele return True - def get_workflow_svg_from_id(self, trans, id, version=None, for_embed=False) -> bytes: + def get_workflow_svg_from_id(self, trans: ProvidesHistoryContext, id, version=None, for_embed=False) -> bytes: stored = self.get_stored_accessible_workflow(trans, id) workflow = stored.get_internal_version(version) return self.get_workflow_svg(trans, workflow, for_embed=for_embed) - def get_workflow_svg(self, trans, workflow, for_embed=False) -> bytes: + def get_workflow_svg(self, trans: ProvidesHistoryContext, workflow, for_embed=False) -> bytes: try: svg = self._workflow_to_svg_canvas(trans, workflow, for_embed=for_embed) s = STANDALONE_SVG_TEMPLATE % svg.tostring() @@ -407,7 +411,7 @@ class WorkflowsManager(sharable.SharableModelManager[model.StoredWorkflow], dele ) raise exceptions.MessageException(message) - def _workflow_to_svg_canvas(self, trans, workflow, for_embed=False): + def _workflow_to_svg_canvas(self, trans: ProvidesHistoryContext, workflow, for_embed=False): workflow_canvas = WorkflowCanvas() for step in workflow.steps: # Load from database representation @@ -425,7 +429,7 @@ class WorkflowsManager(sharable.SharableModelManager[model.StoredWorkflow], dele return workflow_canvas.finish(for_embed=for_embed) def get_invocation( - self, trans, decoded_invocation_id: int, check_ownership=True, check_accessible=True + self, trans: ProvidesUserContext, decoded_invocation_id: int, check_ownership=True, check_accessible=True ) -> WorkflowInvocation: workflow_invocation = _get_invocation(trans.sa_session, decoded_invocation_id) if not workflow_invocation: @@ -437,7 +441,7 @@ class WorkflowsManager(sharable.SharableModelManager[model.StoredWorkflow], dele ) return workflow_invocation - def get_invocation_report(self, trans, invocation_id, **kwd): + def get_invocation_report(self, trans: ProvidesUserContext, invocation_id, **kwd): decoded_workflow_invocation_id = ( trans.security.decode_id(invocation_id) if isinstance(invocation_id, str) else invocation_id ) @@ -458,7 +462,7 @@ class WorkflowsManager(sharable.SharableModelManager[model.StoredWorkflow], dele target_format=target_format, ) - def request_invocation_cancellation(self, trans, decoded_invocation_id: int): + def request_invocation_cancellation(self, trans: ProvidesUserContext, decoded_invocation_id: int): workflow_invocation = self.get_invocation(trans, decoded_invocation_id, check_ownership=True) cancelled = workflow_invocation.cancel() @@ -471,7 +475,11 @@ class WorkflowsManager(sharable.SharableModelManager[model.StoredWorkflow], dele return workflow_invocation def get_invocation_step( - self, trans, decoded_workflow_invocation_step_id, check_ownership: bool = True, check_accessible: bool = True + self, + trans: ProvidesUserContext, + decoded_workflow_invocation_step_id, + check_ownership: bool = True, + check_accessible: bool = True, ) -> WorkflowInvocationStep: try: workflow_invocation_step = trans.sa_session.get(WorkflowInvocationStep, decoded_workflow_invocation_step_id) @@ -485,7 +493,7 @@ class WorkflowsManager(sharable.SharableModelManager[model.StoredWorkflow], dele ) return workflow_invocation_step - def update_invocation_step(self, trans, decoded_workflow_invocation_step_id, action): + def update_invocation_step(self, trans: ProvidesHistoryContext, decoded_workflow_invocation_step_id, action): if action is None: raise exceptions.RequestParameterMissingException( "Updating workflow invocation step requires an action parameter. " @@ -664,7 +672,7 @@ class WorkflowContentsManager(UsesAnnotations): created_workflow = self.build_workflow_from_raw_description(trans, raw_description, WorkflowCreateOptions()) return created_workflow.workflow - def normalize_workflow_format(self, trans, as_dict): + def normalize_workflow_format(self, trans: ProvidesUserContext, as_dict): """Process incoming workflow descriptions for consumption by other methods. Currently this mostly means converting format 2 workflows into standard Galaxy @@ -699,7 +707,7 @@ class WorkflowContentsManager(UsesAnnotations): def build_workflow_from_raw_description( self, - trans, + trans: ProvidesHistoryContext, raw_workflow_description, workflow_create_options, source=None, @@ -763,7 +771,7 @@ class WorkflowContentsManager(UsesAnnotations): def update_workflow_from_raw_description( self, - trans, + trans: ProvidesHistoryContext, stored_workflow: StoredWorkflow, raw_workflow_description: RawWorkflowDescription, workflow_update_options: WorkflowUpdateOptions, @@ -836,7 +844,7 @@ class WorkflowContentsManager(UsesAnnotations): def _workflow_from_raw_description( self, - trans, + trans: ProvidesHistoryContext, raw_workflow_description, workflow_state_resolution_options, name, @@ -976,7 +984,7 @@ class WorkflowContentsManager(UsesAnnotations): def workflow_to_dict( self, - trans, + trans: ProvidesHistoryContext, stored: StoredWorkflow, style: str = "export", version: int | None = None, @@ -1053,7 +1061,7 @@ class WorkflowContentsManager(UsesAnnotations): wf_dict["version"] = len(stored.workflows) - 1 return wf_dict - def _sync_stored_workflow(self, trans, stored_workflow: StoredWorkflow) -> None: + def _sync_stored_workflow(self, trans: ProvidesHistoryContext, stored_workflow: StoredWorkflow) -> None: if trans.user_is_admin: workflow_path = stored_workflow.from_path assert workflow_path is not None @@ -1083,7 +1091,7 @@ class WorkflowContentsManager(UsesAnnotations): workflow_path: str, stored_workflow: StoredWorkflow, workflow: Workflow, - trans=None, + trans: ProvidesHistoryContext | None = None, history: History | None = None, user: User | None = None, ) -> None: @@ -1102,7 +1110,7 @@ class WorkflowContentsManager(UsesAnnotations): f.write(wf_dict["yaml_content"]) def _workflow_to_dict_run( - self, trans: ProvidesUserContext, stored: StoredWorkflow, workflow: Workflow, history: History | None = None + self, trans: ProvidesHistoryContext, stored: StoredWorkflow, workflow: Workflow, history: History | None = None ) -> dict[str, Any]: """ Builds workflow dictionary used by run workflow form @@ -1217,7 +1225,7 @@ class WorkflowContentsManager(UsesAnnotations): "workflow_resource_parameters": self._workflow_resource_parameters(trans, stored, workflow), } - def _workflow_to_dict_preview(self, trans, workflow): + def _workflow_to_dict_preview(self, trans: ProvidesHistoryContext, workflow): """ Builds workflow dictionary containing input labels and values. Used to create embedded workflow previews. @@ -1325,6 +1333,9 @@ class WorkflowContentsManager(UsesAnnotations): continue if step.type == "tool": tool = trans.app.toolbox.get_tool(step.tool_id, step.tool_version) + assert ( + tool is not None + ), f"Tool '{step.tool_id}' unexpectedly missing after successful runtime state computation" step_dict["tool_id"] = step.tool_id step_dict["tool_version"] = step.tool_version step_dict["label"] = step.label or tool.name @@ -1346,13 +1357,13 @@ class WorkflowContentsManager(UsesAnnotations): "steps": step_dicts, } - def _workflow_resource_parameters(self, trans, stored, workflow): + def _workflow_resource_parameters(self, trans: ProvidesUserContext, stored, workflow): """Get workflow scheduling resource parameters for this user and workflow or None if not configured.""" return self._resource_mapper_function(trans=trans, stored_workflow=stored, workflow=workflow) def _workflow_to_dict_editor( self, - trans, + trans: ProvidesHistoryContext, stored: StoredWorkflow | None, workflow: Workflow, tooltip: bool = True, @@ -1585,7 +1596,7 @@ class WorkflowContentsManager(UsesAnnotations): def _workflow_to_dict_export( self, - trans, + trans: ProvidesHistoryContext, workflow: Workflow, stored: StoredWorkflow | None = None, internal: bool = False, @@ -1833,12 +1844,13 @@ class WorkflowContentsManager(UsesAnnotations): return data def _workflow_to_dict_instance( - self, trans, stored: StoredWorkflow, workflow: Workflow, legacy: bool = True + self, trans: ProvidesAppContext, stored: StoredWorkflow, workflow: Workflow, legacy: bool = True ) -> dict[str, Any]: encode = self.app.security.encode_id sa_session = self.app.model.context item = stored.to_dict(view="element") item["name"] = workflow.name + assert trans.url_builder item["url"] = trans.url_builder("workflow", id=encode(stored.id)) item["owner"] = stored.user.username item["email_hash"] = md5_hash_str(stored.user.email) @@ -1976,7 +1988,7 @@ class WorkflowContentsManager(UsesAnnotations): def __load_subworkflows( self, - trans, + trans: ProvidesHistoryContext, step_dict, subworkflow_id_map, workflow_state_resolution_options, @@ -1997,7 +2009,7 @@ class WorkflowContentsManager(UsesAnnotations): def __module_from_dict( self, - trans, + trans: ProvidesHistoryContext, steps: list[model.WorkflowStep], steps_by_external_id: dict[str, model.WorkflowStep], step_dict, @@ -2083,7 +2095,7 @@ class WorkflowContentsManager(UsesAnnotations): def __load_subworkflow_from_step_dict( self, - trans, + trans: ProvidesHistoryContext, step_dict, subworkflow_id_map, workflow_state_resolution_options, @@ -2147,7 +2159,7 @@ class WorkflowContentsManager(UsesAnnotations): def __build_subworkflow_from_url( self, - trans, + trans: ProvidesHistoryContext, url: str, resolving_urls: frozenset[str], ) -> model.Workflow: @@ -2173,7 +2185,7 @@ class WorkflowContentsManager(UsesAnnotations): def __build_subworkflow_from_trs_url( self, - trans, + trans: ProvidesHistoryContext, trs_url: str, resolving_urls: frozenset[str], ) -> model.Workflow: @@ -2206,7 +2218,7 @@ class WorkflowContentsManager(UsesAnnotations): def __build_subworkflow_from_trs_id( self, - trans, + trans: ProvidesHistoryContext, step_dict: dict, resolving_urls: frozenset[str], ) -> model.Workflow: @@ -2227,7 +2239,11 @@ class WorkflowContentsManager(UsesAnnotations): return self.__build_subworkflow_from_trs_url(trans, trs_url, resolving_urls) def __build_embedded_subworkflow( - self, trans, data, workflow_state_resolution_options, resolving_urls: frozenset[str] = frozenset() + self, + trans: ProvidesHistoryContext, + data, + workflow_state_resolution_options, + resolving_urls: frozenset[str] = frozenset(), ): raw_workflow_description = self.ensure_raw_description(data) subworkflow = self.build_workflow_from_raw_description( @@ -2290,7 +2306,7 @@ class WorkflowContentsManager(UsesAnnotations): step.label = module.label = default_label def do_refactor( - self, trans: ProvidesUserContext, stored_workflow: StoredWorkflow, refactor_request: RefactorRequest + self, trans: ProvidesHistoryContext, stored_workflow: StoredWorkflow, refactor_request: RefactorRequest ): """Apply supplied actions to either the latest version of the workflow or a specific version to build a new version.""" # Get the workflow version to refactor (latest or specific version) @@ -2323,7 +2339,9 @@ class WorkflowContentsManager(UsesAnnotations): # we send back anyway return refactored_workflow, action_executions - def refactor(self, trans: ProvidesUserContext, stored_workflow: StoredWorkflow, refactor_request: RefactorRequest): + def refactor( + self, trans: ProvidesHistoryContext, stored_workflow: StoredWorkflow, refactor_request: RefactorRequest + ): refactored_workflow, action_executions = self.do_refactor(trans, stored_workflow, refactor_request) return RefactorResponse( action_executions=action_executions, @@ -2354,7 +2372,7 @@ class WorkflowContentsManager(UsesAnnotations): def get_or_create_workflow_from_trs( self, - trans: ProvidesUserContext, + trans: ProvidesHistoryContext, trs_url: str | None, trs_id: str | None = None, trs_version: str | None = None, @@ -2376,7 +2394,7 @@ class WorkflowContentsManager(UsesAnnotations): return workflow def create_workflow_from_trs_url( - self, trans: ProvidesUserContext, trs_url: str, trs_server: str | None = None + self, trans: ProvidesHistoryContext, trs_url: str, trs_server: str | None = None ) -> StoredWorkflow: _, trs_tool_id, trs_version_id = self.trs_proxy.get_trs_id_and_version_from_trs_url(trs_url=trs_url) data = self.trs_proxy.get_version_from_trs_url(trs_url) @@ -2395,7 +2413,7 @@ class WorkflowContentsManager(UsesAnnotations): ) return created_workflow.stored_workflow - def get_or_create_workflow_from_url(self, trans: ProvidesUserContext, url: str) -> StoredWorkflow: + def get_or_create_workflow_from_url(self, trans: ProvidesHistoryContext, url: str) -> StoredWorkflow: """Fetch and import a workflow from an arbitrary URL. Supports various URL schemes including http://, https://, and base64://. diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index ef8363f72d5..399ca32d27d 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -241,12 +241,17 @@ from galaxy.util.sanitize_html import sanitize_html if TYPE_CHECKING: from sqlalchemy.sql.expression import BindParameter + from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesUserContext, + ) from galaxy.objectstore import ( BaseObjectStore, ObjectStorePopulator, QuotaSourceMap, ) from galaxy.schema.invocation import InvocationMessageUnion + from galaxy.webapps.base.webapp import GalaxyWebTransaction log = logging.getLogger(__name__) @@ -5790,7 +5795,7 @@ class DatasetInstance(RepresentById, UsesCreateAndUpdateTime, _HasTable): return item return None - def get_converted_dataset_deps(self, trans, target_ext, use_cached_job=False): + def get_converted_dataset_deps(self, trans: "ProvidesUserContext", target_ext, use_cached_job=False): """ Returns dict of { "dependency" => HDA } """ @@ -5802,7 +5807,13 @@ class DatasetInstance(RepresentById, UsesCreateAndUpdateTime, _HasTable): return {dep: self.get_converted_dataset(trans, dep, use_cached_job=use_cached_job) for dep in depends_list} def get_converted_dataset( - self, trans, target_ext, target_context=None, history=None, include_errored=False, use_cached_job=False + self, + trans: "ProvidesUserContext", + target_ext, + target_context=None, + history=None, + include_errored=False, + use_cached_job=False, ): """ Return converted dataset(s) if they exist, along with a dict of dependencies. @@ -5897,7 +5908,7 @@ class DatasetInstance(RepresentById, UsesCreateAndUpdateTime, _HasTable): return format in self.get_converter_types() def find_conversion_destination( - self, accepted_formats: list[str], **kwd + self, accepted_formats: Iterable[Union[str, "Data"]], **kwd ) -> tuple[bool, str | None, Optional["DatasetInstance"]]: """Returns ( target_ext, existing converted dataset )""" return self.datatype.find_conversion_destination(self, accepted_formats, _get_datatypes_registry(), **kwd) @@ -5992,10 +6003,10 @@ class DatasetInstance(RepresentById, UsesCreateAndUpdateTime, _HasTable): return creating_job_associations[0].job return None - def get_display_applications(self, trans): + def get_display_applications(self, trans: "GalaxyWebTransaction"): return self.datatype.get_display_applications_by_dataset(self, trans) - def get_datasources(self, trans): + def get_datasources(self, trans: "ProvidesUserContext"): """ Returns datasources for dataset; if datasources are not available due to indexing, indexing is started. Return value is a dictionary @@ -6011,25 +6022,19 @@ class DatasetInstance(RepresentById, UsesCreateAndUpdateTime, _HasTable): msg = None data_source = source_list else: - # Convert. - if isinstance(source_list, str): - source_list = [source_list] - - # Loop through sources until viable one is found. - for source in source_list: - msg = self.convert_dataset(trans, source) - # No message or PENDING means that source is viable. No - # message indicates conversion was done and is successful. - if not msg or msg == self.conversion_messages.PENDING: - data_source = source - break + # Convert. Each data_sources entry names a single source. + msg = self.convert_dataset(trans, source_list) + # No message or PENDING means that source is viable. No + # message indicates conversion was done and is successful. + if not msg or msg == self.conversion_messages.PENDING: + data_source = source_list # Store msg. data_sources_dict[source_type] = {"name": data_source, "message": msg} return data_sources_dict - def convert_dataset(self, trans, target_type): + def convert_dataset(self, trans: "ProvidesUserContext", target_type): """ Converts a dataset to the target_type and returns a message indicating status of the conversion. None is returned to indicate that dataset @@ -6046,7 +6051,7 @@ class DatasetInstance(RepresentById, UsesCreateAndUpdateTime, _HasTable): return {"kind": self.conversion_messages.ERROR, "message": dep_error.value} # Check dataset state and return any messages. - msg = None + msg: dict[str, Any] | Dataset.conversion_messages | None = None if converted_dataset and converted_dataset.state == Dataset.states.ERROR: stmt = select(JobToOutputDatasetAssociation.job_id).filter_by(dataset_id=converted_dataset.id).limit(1) job_id = trans.sa_session.scalars(stmt).first() @@ -6258,7 +6263,7 @@ class HistoryDatasetAssociation(DatasetInstance, HasTags, UsesAnnotations, HasNa def to_library_dataset_dataset_association( self, - trans, + trans: "ProvidesUserContext", target_folder, replace_dataset=None, parent_id=None, @@ -11421,7 +11426,7 @@ class UserAddress(Base, RepresentById): # TODO: db migration to rename column, then use `desc` user: Mapped[Optional["User"]] = relationship(back_populates="addresses", order_by=sqlalchemy.desc("update_time")) - def to_dict(self, trans): + def to_dict(self, trans: "ProvidesAppContext"): return { "id": trans.security.encode_id(self.id), "name": sanitize_html(self.name), diff --git a/lib/galaxy/model/security.py b/lib/galaxy/model/security.py index 38b17a5e69e..439248d4e1a 100644 --- a/lib/galaxy/model/security.py +++ b/lib/galaxy/model/security.py @@ -2,6 +2,7 @@ import logging import socket import sqlite3 from datetime import timedelta +from typing import TYPE_CHECKING from sqlalchemy import ( and_, @@ -57,6 +58,12 @@ from galaxy.util import ( ) from galaxy.util.bunch import Bunch +if TYPE_CHECKING: + from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesUserContext, + ) + log = logging.getLogger(__name__) @@ -89,7 +96,7 @@ class GalaxyRBACAgent(RBACAgent): intermed.sort() return [_[-1] for _ in intermed] - def get_all_roles(self, trans, cntrller): + def get_all_roles(self, trans: "ProvidesUserContext", cntrller): admin_controller = cntrller in ["library_admin"] roles = set() if not trans.user: @@ -123,7 +130,9 @@ class GalaxyRBACAgent(RBACAgent): roles.append(item_permission.role) return roles - def get_valid_roles(self, trans, item, query=None, page=None, page_limit=None, is_library_access=False): + def get_valid_roles( + self, trans: "ProvidesUserContext", item, query=None, page=None, page_limit=None, is_library_access=False + ): """ This method retrieves the list of possible roles that user can select in the item permissions form. Admins can select any role so the @@ -196,7 +205,7 @@ class GalaxyRBACAgent(RBACAgent): total_count = len(return_roles) return self.sort_by_attr(list(return_roles), "name"), total_count - def get_legitimate_roles(self, trans, item, cntrller): + def get_legitimate_roles(self, trans: "ProvidesUserContext", item, cntrller): """ Return a sorted list of legitimate roles that can be associated with a permission on item where item is a Library or a Dataset. The cntrller param is the controller from @@ -290,7 +299,7 @@ class GalaxyRBACAgent(RBACAgent): break return ret_val - def get_actions_for_items(self, trans, action, permission_items): + def get_actions_for_items(self, trans: "ProvidesAppContext", action, permission_items): # TODO: Rename this; it's a replacement for get_item_actions, but it # doesn't represent what it's really doing, which is confusing. # TODO: Make this work for other classes besides lib_datasets. @@ -309,18 +318,18 @@ class GalaxyRBACAgent(RBACAgent): # # If the dataset id has no corresponding action in its permissions, # then the returned permissions will not carry an entry for the dataset. - ret_permissions = {} + ret_permissions: dict[int, list] = {} if len(permission_items) > 0: # SM: NB: LibraryDatasets became Datasets for some odd reason. if isinstance(permission_items[0], LibraryDataset): ids = [item.library_dataset_id for item in permission_items] - stmt = select(LibraryDatasetPermissions).where( + library_dataset_stmt = select(LibraryDatasetPermissions).where( and_( LibraryDatasetPermissions.library_dataset_id.in_(ids), LibraryDatasetPermissions.action == action.action, ) ) - permissions = trans.sa_session.scalars(stmt) + permissions = trans.sa_session.scalars(library_dataset_stmt) # Massage the return data. We will return a list of permissions # for each library dataset. So we initialize the return list to # have an empty list for each dataset. Then each permission is @@ -334,10 +343,10 @@ class GalaxyRBACAgent(RBACAgent): elif isinstance(permission_items[0], Dataset): ids = [item.id for item in permission_items] - stmt = select(DatasetPermissions).where( + dataset_stmt = select(DatasetPermissions).where( and_(DatasetPermissions.dataset_id.in_(ids), DatasetPermissions.action == action.action) ) - permissions = trans.sa_session.scalars(stmt) + permissions = trans.sa_session.scalars(dataset_stmt) # Massage the return data. We will return a list of permissions # for each library dataset. So we initialize the return list to # have an empty list for each dataset. Then each permission is @@ -384,7 +393,7 @@ class GalaxyRBACAgent(RBACAgent): return ret_permissions - def allow_action_on_libitems(self, trans, user_roles, action, items): + def allow_action_on_libitems(self, trans: "ProvidesAppContext", user_roles, action, items): """ This should be the equivalent of allow_action defined on multiple items. It is meant to specifically replace allow_action for multiple @@ -437,7 +446,7 @@ class GalaxyRBACAgent(RBACAgent): return ret_allow_action # DELETEME: SM: DO NOT TOUCH! This actually works. - def dataset_access_mapping(self, trans, user_roles, datasets): + def dataset_access_mapping(self, trans: "ProvidesAppContext", user_roles, datasets): """ For the given list of datasets, return a mapping of the datasets' ids to whether they can be accessed by the user or not. The datasets input @@ -452,7 +461,7 @@ class GalaxyRBACAgent(RBACAgent): can_access[dataset.id] = datasets_public_map[dataset.id] or datasets_allow_action_map[dataset.id] return can_access - def dataset_permission_map_for_access(self, trans, user_roles, libitems): + def dataset_permission_map_for_access(self, trans: "ProvidesAppContext", user_roles, libitems): """ For a given list of library items (e.g., Datasets), return a map of the datasets' ids to whether they can have permission to use that action @@ -477,13 +486,13 @@ class GalaxyRBACAgent(RBACAgent): can_access[libitem.id] = libitems_public_map[libitem.id] or libitems_allow_action_map[libitem.id] return can_access - def item_permission_map_for_modify(self, trans, user_roles, libitems): + def item_permission_map_for_modify(self, trans: "ProvidesAppContext", user_roles, libitems): return self.allow_action_on_libitems(trans, user_roles, self.permitted_actions.LIBRARY_MODIFY, libitems) - def item_permission_map_for_manage(self, trans, user_roles, libitems): + def item_permission_map_for_manage(self, trans: "ProvidesAppContext", user_roles, libitems): return self.allow_action_on_libitems(trans, user_roles, self.permitted_actions.LIBRARY_MANAGE, libitems) - def item_permission_map_for_add(self, trans, user_roles, libitems): + def item_permission_map_for_add(self, trans: "ProvidesAppContext", user_roles, libitems): return self.allow_action_on_libitems(trans, user_roles, self.permitted_actions.LIBRARY_ADD, libitems) def can_access_dataset(self, user_roles, dataset: Dataset): @@ -519,26 +528,28 @@ class GalaxyRBACAgent(RBACAgent): roles, self.permitted_actions.LIBRARY_ACCESS, library ) - def get_accessible_libraries(self, trans, user): + def get_accessible_libraries(self, trans: "ProvidesAppContext", user): """Return all data libraries that the received user can access""" accessible_libraries = [] current_user_role_ids = [role.id for role in user.all_roles()] library_access_action = self.permitted_actions.LIBRARY_ACCESS.action - stmt = select(LibraryPermissions).where(LibraryPermissions.action == library_access_action).distinct() - restricted_library_ids = [lp.library_id for lp in trans.sa_session.scalars(stmt)] + restricted_stmt = ( + select(LibraryPermissions).where(LibraryPermissions.action == library_access_action).distinct() + ) + restricted_library_ids = [lp.library_id for lp in trans.sa_session.scalars(restricted_stmt)] - stmt = select(LibraryPermissions).where( + accessible_stmt = select(LibraryPermissions).where( and_( LibraryPermissions.action == library_access_action, LibraryPermissions.role_id.in_(current_user_role_ids), ) ) - accessible_restricted_library_ids = [lp.library_id for lp in trans.sa_session.scalars(stmt)] + accessible_restricted_library_ids = [lp.library_id for lp in trans.sa_session.scalars(accessible_stmt)] # Filter to get libraries accessible by the current user. Get both # public libraries and restricted libraries accessible by the current user. - stmt = ( + library_stmt = ( select(Library) .where( and_( @@ -551,11 +562,11 @@ class GalaxyRBACAgent(RBACAgent): ) .order_by(Library.name) ) - for library in trans.sa_session.scalars(stmt): + for library in trans.sa_session.scalars(library_stmt): accessible_libraries.append(library) return accessible_libraries - def has_accessible_folders(self, trans, folder, user, roles, search_downward=True): + def has_accessible_folders(self, trans: "ProvidesAppContext", folder, user, roles, search_downward=True): if ( self.has_accessible_library_datasets(trans, folder, user, roles, search_downward=search_downward) or self.can_add_library_item(roles, folder) @@ -568,7 +579,7 @@ class GalaxyRBACAgent(RBACAgent): return self.has_accessible_folders(trans, active_folder, user, roles, search_downward=search_downward) return False - def has_accessible_library_datasets(self, trans, folder, user, roles, search_downward=True): + def has_accessible_library_datasets(self, trans: "ProvidesAppContext", folder, user, roles, search_downward=True): stmt = select(LibraryDataset).where( and_(LibraryDataset.deleted == false(), LibraryDataset.folder_id == folder.id) ) @@ -579,7 +590,7 @@ class GalaxyRBACAgent(RBACAgent): return self.__active_folders_have_accessible_library_datasets(trans, folder, user, roles) return False - def __active_folders_have_accessible_library_datasets(self, trans, folder, user, roles): + def __active_folders_have_accessible_library_datasets(self, trans: "ProvidesAppContext", folder, user, roles): for active_folder in folder.active_folders: if self.has_accessible_library_datasets(trans, active_folder, user, roles): return True @@ -996,7 +1007,7 @@ WHERE history.user_id != :user_id and history_dataset_association.dataset_id = : self.associate_user_role(user, sharing_role) return sharing_role - def set_all_library_permissions(self, trans, library_item, permissions=None): + def set_all_library_permissions(self, trans: "ProvidesAppContext", library_item, permissions=None): # Set new permissions on library_item, eliminating all current permissions flush_needed = False permissions = permissions or {} @@ -1016,6 +1027,7 @@ WHERE history.user_id != :user_id and history_dataset_association.dataset_id = : # Permission setting related to DATASET_MANAGE_PERMISSIONS was broken for a period of time, # so it is possible that some Datasets have no roles associated with the DATASET_MANAGE_PERMISSIONS # permission. In this case, we'll reset this permission to the library_item user's private role. + assert library_item.dataset is not None if not library_item.dataset.has_manage_permissions_roles(self): # Well this looks like a bug, this should be looked at. # Default permissions above is single hash that keeps getting reeditted here @@ -1120,14 +1132,14 @@ WHERE history.user_id != :user_id and history_dataset_association.dataset_id = : """ return self.permitted_actions.DATASET_ACCESS.action not in [a.action for a in dataset.actions] - def dataset_is_unrestricted(self, trans, dataset): + def dataset_is_unrestricted(self, trans: "ProvidesAppContext", dataset): """ Different implementation of the method above with signature: def dataset_is_public( self, dataset ) """ return len(dataset.library_dataset_dataset_association.get_access_roles(self)) == 0 - def dataset_is_private_to_user(self, trans, dataset): + def dataset_is_private_to_user(self, trans: "ProvidesUserContext", dataset): """ If the Dataset object has exactly one access role and that is the current user's private role then we consider the dataset private. @@ -1156,7 +1168,7 @@ WHERE history.user_id != :user_id and history_dataset_association.dataset_id = : access_role = access_roles[0] return access_role.type == Role.types.PRIVATE - def datasets_are_public(self, trans, datasets): + def datasets_are_public(self, trans: "ProvidesAppContext", datasets): """ Given a transaction object and a list of Datasets, return a mapping from Dataset ids to whether the Dataset is public @@ -1197,12 +1209,12 @@ WHERE history.user_id != :user_id and history_dataset_association.dataset_id = : if flush_needed: self.sa_session.commit() - def derive_roles_from_access(self, trans, item_id, cntrller, library=False, **kwd): + def derive_roles_from_access(self, trans: "ProvidesUserContext", item_id, cntrller, library=False, **kwd): # Check the access permission on a dataset. If library is true, item_id refers to a library. If library # is False, item_id refers to a dataset ( item_id must currently be decoded before being sent ). The # cntrller param is the calling controller, which needs to be passed to get_legitimate_roles(). msg = "" - permissions = {} + permissions: dict = {} # accessible will be True only if at least 1 user has every role in DATASET_ACCESS_in accessible = False # legitimate will be True only if all roles in DATASET_ACCESS_in are in the set of roles returned from @@ -1296,9 +1308,11 @@ WHERE history.user_id != :user_id and history_dataset_association.dataset_id = : permissions[self.get_action(v.action)] = in_roles return permissions, in_roles, error, msg - def copy_library_permissions(self, trans, source_library_item, target_library_item, user=None): + def copy_library_permissions( + self, trans: "ProvidesAppContext", source_library_item, target_library_item, user=None + ): # Copy all relevant permissions from source. - permissions = {} + permissions: dict = {} for role_assoc in source_library_item.actions: if role_assoc.action != self.permitted_actions.LIBRARY_ACCESS.action: # LIBRARY_ACCESS is a special permission that is set only at the library level. @@ -1324,7 +1338,7 @@ WHERE history.user_id != :user_id and history_dataset_association.dataset_id = : self.sa_session.add(lp) self.sa_session.commit() - def get_permitted_libraries(self, trans, user, actions): + def get_permitted_libraries(self, trans: "ProvidesAppContext", user, actions): """ This method is historical (it is not currently used), but may be useful again at some point. It returns a dictionary whose keys are library objects and whose values are a diff --git a/lib/galaxy/security/__init__.py b/lib/galaxy/security/__init__.py index 63a4d5a092a..1b433d5bdc4 100644 --- a/lib/galaxy/security/__init__.py +++ b/lib/galaxy/security/__init__.py @@ -5,10 +5,17 @@ Galaxy Security from typing import ( Literal, + TYPE_CHECKING, ) from galaxy.util.bunch import Bunch +if TYPE_CHECKING: + from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesUserContext, + ) + ActionModel = Literal["grant", "restrict"] @@ -111,7 +118,7 @@ class RBACAgent: def set_dataset_permission(self, dataset, permission): raise Exception("Unimplemented Method") - def set_all_library_permissions(self, trans, dataset, permissions): + def set_all_library_permissions(self, trans: "ProvidesAppContext", dataset, permissions): raise Exception("Unimplemented Method") def set_library_item_permission(self, library_item, permission): @@ -123,10 +130,10 @@ class RBACAgent: def make_library_public(self, library): raise Exception("Unimplemented Method") - def get_accessible_libraries(self, trans, user): + def get_accessible_libraries(self, trans: "ProvidesAppContext", user): raise Exception("Unimplemented Method") - def get_permitted_libraries(self, trans, user, actions): + def get_permitted_libraries(self, trans: "ProvidesAppContext", user, actions): raise Exception("Unimplemented Method") def folder_is_public(self, library): @@ -144,13 +151,13 @@ class RBACAgent: def get_permissions(self, library_dataset): raise Exception("Unimplemented Method") - def get_all_roles(self, trans, cntrller): + def get_all_roles(self, trans: "ProvidesUserContext", cntrller): raise Exception("Unimplemented Method") - def get_legitimate_roles(self, trans, item, cntrller): + def get_legitimate_roles(self, trans: "ProvidesUserContext", item, cntrller): raise Exception("Unimplemented Method") - def derive_roles_from_access(self, trans, item_id, cntrller, library=False, **kwd): + def derive_roles_from_access(self, trans: "ProvidesUserContext", item_id, cntrller, library=False, **kwd): raise Exception("Unimplemented Method") def get_component_associations(self, **kwd): diff --git a/lib/galaxy/security/validate_user_input.py b/lib/galaxy/security/validate_user_input.py index 4d827baf6ff..9c0f65d2e3a 100644 --- a/lib/galaxy/security/validate_user_input.py +++ b/lib/galaxy/security/validate_user_input.py @@ -7,6 +7,11 @@ user inputs - so these methods do not need to be escaped. import logging import re +from typing import ( + Any, + Protocol, + TYPE_CHECKING, +) import dns.resolver from dns.exception import DNSException @@ -18,6 +23,26 @@ from typing_extensions import LiteralString from galaxy.objectstore import ObjectStore +if TYPE_CHECKING: + from galaxy.model import User + + +class UserValidationContext(Protocol): + """What the user input validators need from a transaction. + + Galaxy and the tool shed keep separate context hierarchies with their own + app, config and User classes. Both offer a session and an app, so the + validators ask for that much rather than naming either hierarchy and + forcing one side to depend on the other. + """ + + @property + def app(self) -> Any: ... + + @property + def sa_session(self) -> Any: ... + + log = logging.getLogger(__name__) # Email validity parameters @@ -75,7 +100,9 @@ def validate_publicname_str(publicname): return "" -def validate_email(trans, email, user=None, check_dup=True, allow_empty=False, validate_domain=False): +def validate_email( + trans: UserValidationContext, email, user=None, check_dup=True, allow_empty=False, validate_domain=False +): """ Validates the email format. Checks whether the domain is blocklisted in the disposable domains configuration. @@ -134,7 +161,7 @@ def extract_domain(email, base_only=False): return domain -def validate_publicname(trans, publicname, user=None): +def validate_publicname(trans: UserValidationContext, publicname, user=None): """ Check that publicname respects the minimum and maximum string length, the allowed characters, and that the username is not taken already. @@ -165,14 +192,16 @@ def transform_publicname(publicname): return publicname -def validate_password(trans, password, confirm): +def validate_password(trans: UserValidationContext, password, confirm): if password != confirm: return "Passwords do not match." return validate_password_str(password) -def validate_preferred_object_store_id(trans, object_store: ObjectStore, preferred_object_store_id: str | None) -> str: - return object_store.validate_selected_object_store_id(trans.user, preferred_object_store_id) or "" +def validate_preferred_object_store_id( + user: "User | None", object_store: ObjectStore, preferred_object_store_id: str | None +) -> str: + return object_store.validate_selected_object_store_id(user, preferred_object_store_id) or "" def is_email_banned(email: str, filepath: str | None, canonical_email_rules: dict | None) -> bool: diff --git a/lib/galaxy/structured_app/__init__.py b/lib/galaxy/structured_app/__init__.py index a9f6d993a77..b001e9c2f43 100644 --- a/lib/galaxy/structured_app/__init__.py +++ b/lib/galaxy/structured_app/__init__.py @@ -122,6 +122,7 @@ class MinimalApp(BasicSharedApp): class MinimalManagerApp(MinimalApp): # Minimal App that is sufficient to run Celery tasks amqp_internal_connection_obj: Connection | None + vault: Vault execution_timer_factory: "ExecutionTimerFactory" carbon_intensity: float file_sources: ConfiguredFileSources @@ -174,7 +175,6 @@ class StructuredApp(MinimalManagerApp): tool_dependency_dir: str | None test_data_resolver: test_data.TestDataResolver trs_proxy: TrsProxy - vault: Vault webhooks_registry: WebhooksRegistry queue_worker: Any # 'galaxy.queue_worker.GalaxyQueueWorker' data_provider_registry: Any # 'galaxy.visualization.data_providers.registry.DataProviderRegistry' diff --git a/lib/galaxy/tool_shed/util/shed_util_common.py b/lib/galaxy/tool_shed/util/shed_util_common.py index 90b8079f8a0..d8363e61d0a 100644 --- a/lib/galaxy/tool_shed/util/shed_util_common.py +++ b/lib/galaxy/tool_shed/util/shed_util_common.py @@ -3,6 +3,7 @@ import re from urllib.parse import quote from galaxy import util +from galaxy.managers.context import ProvidesAppContext from galaxy.tool_shed.util import repository_util from galaxy.util.tool_shed import common_util @@ -29,7 +30,7 @@ def can_eliminate_repository_dependency(metadata_dict, tool_shed_url, name, owne return True -def clean_dependency_relationships(trans, metadata_dict, tool_shed_repository, tool_shed_url): +def clean_dependency_relationships(trans: ProvidesAppContext, metadata_dict, tool_shed_repository, tool_shed_url): """ Repositories of type tool_dependency_definition allow for defining a package dependency at some point in the change log and then removing the diff --git a/lib/galaxy/tool_util/data/__init__.py b/lib/galaxy/tool_util/data/__init__.py index 8a5d0c8fa56..686305653b9 100644 --- a/lib/galaxy/tool_util/data/__init__.py +++ b/lib/galaxy/tool_util/data/__init__.py @@ -327,6 +327,12 @@ class ToolDataTable(Dictifiable): def is_current_version(self, other_version): return self._loaded_content_version == other_version + def get_fields(self) -> list[list[str]]: + raise NotImplementedError("Abstract method") + + def get_named_fields_list(self) -> list[dict[str | int, str]]: + raise NotImplementedError("Abstract method") + def merge_tool_data_table( self, other_table: "ToolDataTable", diff --git a/lib/galaxy/tool_util/toolbox/base.py b/lib/galaxy/tool_util/toolbox/base.py index 33e47744b18..c21271cfb74 100644 --- a/lib/galaxy/tool_util/toolbox/base.py +++ b/lib/galaxy/tool_util/toolbox/base.py @@ -12,6 +12,7 @@ from typing import ( Literal, Optional, TYPE_CHECKING, + TypeAlias, Union, ) from urllib.parse import urlparse @@ -70,6 +71,10 @@ from .views.interface import ( from .views.static import StaticToolPanelView if TYPE_CHECKING: + from galaxy.managers.context import ( + ProvidesHistoryContext, + ProvidesUserContext, + ) from galaxy.model import ( DynamicTool, User, @@ -78,6 +83,11 @@ if TYPE_CHECKING: from galaxy.model.tool_shed_install import ToolShedRepository from galaxy.tools import Tool from galaxy.tools.cache import ToolCache + from galaxy.webapps.base.webapp import GalaxyWebTransaction + from galaxy.work.context import SessionRequestContext + + # both web transactions and FastAPI/agents request contexts render tool panels + PanelViewTrans: TypeAlias = "GalaxyWebTransaction | SessionRequestContext" log = logging.getLogger(__name__) @@ -315,7 +325,7 @@ class AbstractToolBox(ManagesIntegratedToolPanelMixin): if save_integrated_tool_panel: self._save_integrated_tool_panel() - def _default_panel_view(self, trans): + def _default_panel_view(self, trans: "PanelViewTrans"): config = self.app.config if hasattr(config, "config_value_for_host"): config_value = config.config_value_for_host("default_panel_view", trans.host) @@ -1366,7 +1376,7 @@ class AbstractToolBox(ManagesIntegratedToolPanelMixin): self._tool_edam_topics = self._collect_tool_attribute_set("edam_topics") return self._tool_edam_topics - def package_tool(self, trans, tool_id): + def package_tool(self, trans: "GalaxyWebTransaction", tool_id): """ Create a tarball with the tool's xml, help images, and test data. :param trans: the web transaction @@ -1471,7 +1481,7 @@ class AbstractToolBox(ManagesIntegratedToolPanelMixin): tool_panel_section_id = "" return tool_panel_section_id - def tool_panel_contents(self, trans, view=None, **kwds): + def tool_panel_contents(self, trans: "PanelViewTrans", view=None, **kwds): """Filter tool_panel contents for displaying for user.""" if view is None: view = self._default_panel_view(trans) @@ -1495,7 +1505,9 @@ class AbstractToolBox(ManagesIntegratedToolPanelMixin): if elt: yield elt - def get_tool_to_dict(self, trans, tool: "Tool", tool_help: bool = False) -> dict[str, Any]: + def get_tool_to_dict( + self, trans: "ProvidesHistoryContext", tool: "Tool", tool_help: bool = False + ) -> dict[str, Any]: """Return tool's panel payload. Use cache if present, store to cache otherwise. Note: The cached payload is specific to the calls from toolbox. @@ -1524,7 +1536,7 @@ class AbstractToolBox(ManagesIntegratedToolPanelMixin): def to_dict( self, - trans, + trans: "PanelViewTrans", in_panel: bool = True, tool_help: bool = False, view: str | None = None, @@ -1553,7 +1565,7 @@ class AbstractToolBox(ManagesIntegratedToolPanelMixin): rval.append(self.get_tool_to_dict(trans, tool, tool_help=tool_help)) return rval - def to_panel_view(self, trans, view="default_panel_view", **kwds): + def to_panel_view(self, trans: "PanelViewTrans", view="default_panel_view", **kwds): """ Create a panel view representation of the toolbox. Uses the structure: @@ -1602,7 +1614,7 @@ class AbstractToolBox(ManagesIntegratedToolPanelMixin): else: return self._tool_versions_by_id.get(lineage_tool_version.id, {}).get(lineage_tool_version.version) - def _build_filter_method(self, trans): + def _build_filter_method(self, trans: "ProvidesUserContext"): context = Bunch(toolbox=self, trans=trans) filters = self._filter_factory.build_filters(trans) return lambda element, item_type: _filter_for_panel(element, item_type, filters, context) diff --git a/lib/galaxy/tool_util/toolbox/filters/__init__.py b/lib/galaxy/tool_util/toolbox/filters/__init__.py index 8a0c8d4aef2..6f092a1304c 100644 --- a/lib/galaxy/tool_util/toolbox/filters/__init__.py +++ b/lib/galaxy/tool_util/toolbox/filters/__init__.py @@ -4,10 +4,14 @@ from copy import deepcopy from typing import ( Protocol, runtime_checkable, + TYPE_CHECKING, ) from galaxy.util import listify +if TYPE_CHECKING: + from galaxy.managers.context import ProvidesUserContext + log = logging.getLogger(__name__) @@ -63,7 +67,7 @@ class FilterFactory: self.__init_filters("section", getattr(config, "tool_section_filters", ""), self.default_filters) self.__init_filters("label", getattr(config, "tool_label_filters", ""), self.default_filters) - def build_filters(self, trans, **kwds): + def build_filters(self, trans: "ProvidesUserContext", **kwds): """ Build list of filters to check tools against given current context. """ diff --git a/lib/galaxy/tool_util/toolbox/panel.py b/lib/galaxy/tool_util/toolbox/panel.py index 7b9367fe40e..7d4fd782ab8 100644 --- a/lib/galaxy/tool_util/toolbox/panel.py +++ b/lib/galaxy/tool_util/toolbox/panel.py @@ -10,6 +10,7 @@ from galaxy.util.odict import odict from .parser import ensure_tool_conf_item if TYPE_CHECKING: + from galaxy.managers.context import ProvidesHistoryContext from galaxy.tools import Tool @@ -99,7 +100,9 @@ class ToolSection(UsesDictVisibleKeys, HasPanelItems): return copy - def to_dict(self, trans, link_details=False, tool_help=False, toolbox=None, only_ids=False): + def to_dict( + self, trans: "ProvidesHistoryContext", link_details=False, tool_help=False, toolbox=None, only_ids=False + ): """Return a dict that includes section's attributes. if `only_ids` is `True`, we store only the ids of the section's tools in `section.tools` diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 4fb8afe4dd7..a30892eb4bb 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -267,7 +267,11 @@ if TYPE_CHECKING: from galaxy.app import UniverseApplication from galaxy.jobs import JobToolConfiguration from galaxy.jobs.job_destination import JobDestination - from galaxy.managers.context import ProvidesUserContext + from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesHistoryContext, + ProvidesUserContext, + ) from galaxy.managers.jobs import JobSearch from galaxy.model import ( DynamicTool, @@ -760,7 +764,7 @@ class DefaultToolState: self.rerun_remap_job_id = None self.inputs = {} - def initialize(self, trans, tool): + def initialize(self, trans: "ProvidesHistoryContext", tool): """ Create a new `DefaultToolState` for this tool. It will be initialized with default values for inputs. Grouping elements are filled in recursively. @@ -2102,7 +2106,7 @@ class Tool(UsesDictVisibleKeys, MaybeToolParameterBundle): # outputs? return True - def new_state(self, trans): + def new_state(self, trans: "ProvidesHistoryContext"): """ Create a new `DefaultToolState` for this tool. It will be initialized with default values for inputs. Grouping elements are filled in recursively. @@ -2333,7 +2337,7 @@ class Tool(UsesDictVisibleKeys, MaybeToolParameterBundle): def completed_jobs( self, - trans, + trans: "ProvidesUserContext", use_cached_job: bool, all_params: list[ToolStateJobInstancePopulatedT], ) -> dict[int, Job | None]: @@ -2391,7 +2395,7 @@ class Tool(UsesDictVisibleKeys, MaybeToolParameterBundle): def handle_input( self, - trans, + trans: "ProvidesHistoryContext", incoming: ToolRequestT, history: History | None = None, use_cached_job: bool = DEFAULT_USE_CACHED_JOB, @@ -2480,7 +2484,7 @@ class Tool(UsesDictVisibleKeys, MaybeToolParameterBundle): def handle_single_execution( self, - trans, + trans: "ProvidesHistoryContext", rerun_remap_job_id: int | None, execution_slice: ExecutionSlice, history: History, @@ -2569,7 +2573,7 @@ class Tool(UsesDictVisibleKeys, MaybeToolParameterBundle): params.append(input_param) return params - def get_static_param_values(self, trans): + def get_static_param_values(self, trans: "ProvidesHistoryContext"): """ Returns a map of parameter names and values if the tool does not require any user input. Will raise an exception if any parameter @@ -2589,7 +2593,7 @@ class Tool(UsesDictVisibleKeys, MaybeToolParameterBundle): def execute( self, - trans, + trans: "ProvidesHistoryContext", incoming: ToolStateJobInstancePopulatedT | None = None, history: History | None = None, set_output_hid: bool = DEFAULT_SET_OUTPUT_HID, @@ -2617,7 +2621,7 @@ class Tool(UsesDictVisibleKeys, MaybeToolParameterBundle): def _execute( self, - trans, + trans: "ProvidesHistoryContext", incoming: ToolStateJobInstancePopulatedT | None = None, validated_parameters: JobInternalToolState | None = None, history: History | None = None, @@ -2678,7 +2682,7 @@ class Tool(UsesDictVisibleKeys, MaybeToolParameterBundle): return self.params_from_strings(param_dict, ignore_errors=ignore_errors) def check_and_update_param_values( - self, values, trans, update_values: bool = True, workflow_building_mode: bool = False + self, values, trans: "ProvidesHistoryContext", update_values: bool = True, workflow_building_mode: bool = False ): """ Check that all parameters have values, and fill in with default @@ -3000,7 +3004,7 @@ class Tool(UsesDictVisibleKeys, MaybeToolParameterBundle): os.remove(temp_file) return tarball_archive - def to_panel_entry(self, trans) -> dict[str, Any]: + def to_panel_entry(self, trans: "ProvidesUserContext") -> dict[str, Any]: """The complete per-tool panel/listing payload — see :class:`galaxy.tool_util.toolbox.entry.ToolPanelEntry` for the contract. ``to_dict`` layers io/help extras on top of this. @@ -3041,7 +3045,7 @@ class Tool(UsesDictVisibleKeys, MaybeToolParameterBundle): kwargs["config_file"] = None if not self.config_file else os.path.abspath(self.config_file) return ToolPanelEntry(**kwargs).model_dump(exclude_unset=True) - def to_dict(self, trans, link_details=False, io_details=False, tool_help=False): + def to_dict(self, trans: "ProvidesHistoryContext", link_details=False, io_details=False, tool_help=False): """Returns dict of tool. ``link_details`` is accepted for backwards compatibility and no @@ -3072,7 +3076,7 @@ class Tool(UsesDictVisibleKeys, MaybeToolParameterBundle): def to_json( self, - trans, + trans: "ProvidesHistoryContext", kwd=None, job: Job | None = None, workflow_building_mode=False, @@ -3093,7 +3097,7 @@ class Tool(UsesDictVisibleKeys, MaybeToolParameterBundle): or workflow_building_mode is workflow_building_modes.DISABLED ): # We don't need a history when exporting a workflow for the workflow editor or when downloading a workflow - history = history or trans.get_history() + history = history or trans.history if history is None and job is not None: assert job.history history = self.history_manager.get_owned(job.history.id, trans.user, current_history=trans.history) @@ -3319,7 +3323,7 @@ class Tool(UsesDictVisibleKeys, MaybeToolParameterBundle): raise exceptions.MessageException(unicodify(e)) return message - def get_default_history_by_trans(self, trans, create=False): + def get_default_history_by_trans(self, trans: "ProvidesHistoryContext", create=False): return trans.get_history(create=create) @classmethod @@ -3832,7 +3836,7 @@ class DataManagerTool(OutputParameterJSONTool): else: raise Exception("Unknown data manager mode encountered type...") - def get_default_history_by_trans(self, trans, create=False): + def get_default_history_by_trans(self, trans: "ProvidesHistoryContext", create=False): def _create_data_manager_history(user): history = History(name="Data Manager History (automatically created)", user=user) data_manager_association = model.DataManagerHistoryAssociation(user=user, history=history) @@ -3967,7 +3971,7 @@ class UnzipCollectionTool(DatabaseOperationTool): require_terminal_states = False require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): has_collection = incoming["input"] if hasattr(has_collection, "element_type"): # It is a DCE @@ -3993,7 +3997,7 @@ class ZipCollectionTool(DatabaseOperationTool): require_terminal_states = False require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): forward_o = incoming["input_forward"] reverse_o = incoming["input_reverse"] @@ -4015,7 +4019,7 @@ class CrossProductFlatCollectionTool(DatabaseOperationTool): require_terminal_states = False require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): input_a = incoming["input_a"] input_b = incoming["input_b"] join_identifier = incoming["join_identifier"] @@ -4051,7 +4055,7 @@ class CrossProductNestedCollectionTool(DatabaseOperationTool): require_terminal_states = False require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): input_a = incoming["input_a"] input_b = incoming["input_b"] @@ -4103,7 +4107,7 @@ class BuildListCollectionTool(DatabaseOperationTool): require_terminal_states = False require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): new_elements = {} for i, incoming_repeat in enumerate(incoming["datasets"]): @@ -4134,7 +4138,7 @@ class SplitPairedAndUnpairedTool(DatabaseOperationTool): require_terminal_states = False require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): has_collection = incoming["input"] if hasattr(has_collection, "element_type"): # It is a DCE @@ -4218,7 +4222,7 @@ class ExtractDatasetCollectionTool(DatabaseOperationTool): require_terminal_states = False require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): has_collection = incoming["input"] if hasattr(has_collection, "element_type"): # It is a DCE @@ -4261,7 +4265,7 @@ class MergeCollectionTool(DatabaseOperationTool): require_terminal_states = False require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): input_lists = [] for incoming_repeat in incoming["inputs"]: @@ -4370,7 +4374,7 @@ class FilterDatasetsTool(DatabaseOperationTool): assert isinstance(element_object, model.DatasetInstance) return element_object.is_ok - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): collection = incoming["input"] replacement_dataset = incoming.get("replacement") if hasattr(collection, "element_object"): @@ -4493,7 +4497,7 @@ class FlattenTool(DatabaseOperationTool): require_terminal_states = False require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): hdca = incoming["input"] join_identifier = incoming["join_identifier"] new_elements = {} @@ -4531,7 +4535,7 @@ class NestTool(DatabaseOperationTool): require_terminal_states = False require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): hdca = incoming["input"] new_elements = {} copied_datasets = [] @@ -4563,7 +4567,7 @@ class SortTool(DatabaseOperationTool): require_terminal_states = True require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): hdca = incoming["input"] sorttype = incoming["sort_type"]["sort_type"] new_elements = {} @@ -4631,7 +4635,7 @@ class HarmonizeTool(DatabaseOperationTool): } super().check_inputs_ready(input_datasets, filtered_collections, security) - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): hdca1 = incoming["input1"] hdca2 = incoming.get("input2") elements1 = hdca1.collection.elements @@ -4690,7 +4694,9 @@ class HarmonizeTool(DatabaseOperationTool): output_with_selected_identifiers(old_elements1_dict, "output1") output_with_selected_identifiers(old_elements2_dict, "output2") - def _produce_outputs_with_optional_nulls(self, trans, output_collections, hdca1, elements1, history): + def _produce_outputs_with_optional_nulls( + self, trans: "ProvidesUserContext", output_collections, hdca1, elements1, history + ): """When input2 is not provided, output1 is a copy of input1 and output2 mirrors input1's structure but with expression.json null datasets.""" object_store_populator = ObjectStorePopulator(trans.app, trans.user) @@ -4738,7 +4744,7 @@ class HarmonizeTool(DatabaseOperationTool): propagate_hda_tags=False, ) - def _create_null_dataset(self, trans, history, object_store_populator): + def _create_null_dataset(self, trans: "ProvidesUserContext", history, object_store_populator): """Create a new HDA with expression.json null content (skipped marker).""" null_hda = HistoryDatasetAssociation( extension="expression.json", @@ -4763,7 +4769,7 @@ class HarmonizeTool(DatabaseOperationTool): class RelabelFromFileTool(DatabaseOperationTool): tool_type = "relabel_from_file" - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): hdca = incoming["input"] how_type = incoming["how"]["how_select"] new_labels_dataset_assoc = incoming["how"]["labels"] @@ -4846,7 +4852,7 @@ class RelabelFromFileTool(DatabaseOperationTool): class ApplyRulesTool(DatabaseOperationTool): tool_type = "apply_rules" - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): hdca = incoming["input"] rule_set = RuleSet(incoming["rules"]) copied_datasets = [] @@ -4884,7 +4890,7 @@ class TagFromFileTool(DatabaseOperationTool): # require_terminal_states = True # require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): hdca = incoming["input"] how = incoming["how"] new_tags_dataset_assoc = incoming["tags"] @@ -4961,7 +4967,7 @@ class TagFromFileTool(DatabaseOperationTool): class FilterFromFileTool(DatabaseOperationTool): tool_type = "filter_from_file" - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): hdca = incoming["input"] how_filter = incoming["how"]["how_filter"] filter_dataset_assoc = incoming["how"]["filter_source"] @@ -5017,7 +5023,7 @@ class DuplicateFileToCollectionTool(DatabaseOperationTool): require_terminal_states = False require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): hda = incoming["input"] number = int(incoming["number"]) element_identifier = incoming["element_identifier"] @@ -5042,7 +5048,7 @@ class ConvertSampleSheetTool(DatabaseOperationTool): require_terminal_states = False require_dataset_ok = False - def produce_outputs(self, trans, out_data, output_collections, incoming, history, **kwds): + def produce_outputs(self, trans: "ProvidesUserContext", out_data, output_collections, incoming, history, **kwds): has_collection = incoming["input"] if hasattr(has_collection, "element_type"): # It is a DCE @@ -5117,7 +5123,7 @@ tool_types = {tool_class.tool_type: tool_class for tool_class in TOOL_CLASSES} # ---- Utility classes to be factored out ----------------------------------- -def _rerun_remap_job_id(trans, incoming, tool_id: str | None) -> int | None: +def _rerun_remap_job_id(trans: "ProvidesAppContext", incoming, tool_id: str | None) -> int | None: rerun_remap_job_id = None if "rerun_remap_job_id" in incoming: try: diff --git a/lib/galaxy/tools/actions/__init__.py b/lib/galaxy/tools/actions/__init__.py index b9c90fd504c..6d2437fb6b5 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -24,7 +24,10 @@ from galaxy.exceptions import ( ToolInputsNotReadyException, ) from galaxy.job_execution.actions.post import ActionBox -from galaxy.managers.context import ProvidesHistoryContext +from galaxy.managers.context import ( + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.model import ( Dataset, History, @@ -79,6 +82,7 @@ if TYPE_CHECKING: ) from galaxy.tool_util.parser.output_objects import ToolOutput from galaxy.tools import Tool + from galaxy.webapps.base.webapp import GalaxyWebTransaction log = logging.getLogger(__name__) @@ -97,7 +101,7 @@ class ToolAction: def execute( self, tool: "Tool", - trans, + trans: ProvidesHistoryContext, incoming: ToolStateJobInstancePopulatedT | None = None, history: History | None = None, job_params=None, @@ -122,7 +126,7 @@ class ToolAction: dataset=None, tool=None, on_text=None, - trans=None, + trans: ProvidesHistoryContext | None = None, incoming=None, history=None, params=None, @@ -398,10 +402,12 @@ class DefaultToolAction(ToolAction): tool.visit_inputs(param_values, visitor) return input_dataset_collections - def _check_access(self, tool, trans): + def _check_access(self, tool, trans: ProvidesUserContext): assert tool.allow_user_access(trans.user), f"User ({trans.user}) is not allowed to access this tool." - def _collect_inputs(self, tool, trans, incoming, history, current_user_roles, collection_info): + def _collect_inputs( + self, tool, trans: ProvidesHistoryContext, incoming, history, current_user_roles, collection_info + ): """Collect history as well as input datasets and collections.""" # Set history. if not history: @@ -442,7 +448,7 @@ class DefaultToolAction(ToolAction): def execute( self, tool: "Tool", - trans, + trans: ProvidesHistoryContext, incoming: ToolStateJobInstancePopulatedT | None = None, history: History | None = None, job_params=None, @@ -797,7 +803,7 @@ class DefaultToolAction(ToolAction): job.info = f"Redirected to: {redirect_url}" trans.sa_session.add(job) trans.sa_session.commit() - trans.response.send_redirect(redirect_url) + cast("GalaxyWebTransaction", trans).response.send_redirect(redirect_url) else: if flush_job: # Set HID and add to history. @@ -920,7 +926,7 @@ class DefaultToolAction(ToolAction): def _wrapped_params( self, - trans, + trans: ProvidesHistoryContext, tool: "Tool", incoming: "ToolStateJobInstancePopulatedT", input_datasets: LegacyUnprefixedDict | None = None, @@ -950,7 +956,7 @@ class DefaultToolAction(ToolAction): return on_text_for_dataset_and_collections(dataset_hids=input_hids, collection_hids=collection_hids) def _new_job_for_session( - self, trans, tool: "Tool", history: History | None + self, trans: ProvidesHistoryContext, tool: "Tool", history: History | None ) -> tuple[Job, model.GalaxySession | None]: job = Job() job.galaxy_version = trans.app.config.version_major @@ -995,7 +1001,7 @@ class DefaultToolAction(ToolAction): ) sa_session.add(association) - def _record_inputs(self, trans, tool, job, incoming, inp_data, inp_dataset_collections): + def _record_inputs(self, trans: ProvidesHistoryContext, tool, job, incoming, inp_data, inp_dataset_collections): # FIXME: Don't need all of incoming here, just the defined parameters # from the tool. We need to deal with tools that pass all post # parameters to the command as a special case. @@ -1070,7 +1076,7 @@ class DefaultToolAction(ToolAction): job.add_output_dataset_collection(name, dataset_collection_instance) dataset_collection_instance.job = job - def _record_input_datasets(self, trans, job, inp_data): + def _record_input_datasets(self, trans: ProvidesHistoryContext, job, inp_data): for name, dataset in inp_data.items(): # TODO: figure out why can't pass dataset_id here. job.add_input_dataset(name, dataset=dataset) @@ -1081,7 +1087,7 @@ class DefaultToolAction(ToolAction): dataset=None, tool=None, on_text=None, - trans=None, + trans: ProvidesHistoryContext | None = None, incoming=None, history=None, params=None, @@ -1104,7 +1110,16 @@ class DefaultToolAction(ToolAction): ) def _get_default_data_name( - self, dataset, tool, on_text=None, trans=None, incoming=None, history=None, params=None, job_params=None, **kwd + self, + dataset, + tool, + on_text=None, + trans: ProvidesHistoryContext | None = None, + incoming=None, + history=None, + params=None, + job_params=None, + **kwd, ): name = tool.name if on_text: @@ -1122,7 +1137,7 @@ class OutputCollections: def __init__( self, - trans, + trans: ProvidesHistoryContext, history, tool, tool_action, @@ -1146,8 +1161,8 @@ class OutputCollections: self.incoming = incoming self.params = params self.job_params = job_params - self.out_collections = {} - self.out_collection_instances = {} + self.out_collections: dict[str, DatasetCollection] = {} + self.out_collection_instances: dict[str, HistoryDatasetCollectionAssociation] = {} self.tags = tags # all inherited tags self.hdca_tags = hdca_tags # only tags inherited from input HDCAs diff --git a/lib/galaxy/tools/actions/upload.py b/lib/galaxy/tools/actions/upload.py index d5df6a95d91..bfedfd06819 100644 --- a/lib/galaxy/tools/actions/upload.py +++ b/lib/galaxy/tools/actions/upload.py @@ -4,8 +4,11 @@ import os from galaxy.exceptions import RequestParameterMissingException from galaxy.job_execution.output_collect import copy_collection_metadata_from_target_dict +from galaxy.managers.context import ProvidesHistoryContext from galaxy.model import ( History, + HistoryDatasetAssociation, + HistoryDatasetCollectionAssociation, Job, ) from galaxy.model.dataset_collections.matching import MatchingCollections @@ -39,7 +42,7 @@ class BaseUploadToolAction(ToolAction): def execute( self, tool, - trans, + trans: ProvidesHistoryContext, incoming: ToolStateJobInstancePopulatedT | None = None, history: History | None = None, job_params=None, @@ -69,7 +72,9 @@ class BaseUploadToolAction(ToolAction): rval = self._setup_job(tool, trans, incoming, dataset_upload_inputs, history, preferred_object_store_id) return rval - def _setup_job(self, tool, trans, incoming, dataset_upload_inputs, history, preferred_object_store_id): + def _setup_job( + self, tool, trans: ProvidesHistoryContext, incoming, dataset_upload_inputs, history, preferred_object_store_id + ): """Take persisted uploads and create a job for given tool.""" def _create_job(self, *args, **kwds): @@ -81,7 +86,9 @@ class BaseUploadToolAction(ToolAction): class UploadToolAction(BaseUploadToolAction): - def _setup_job(self, tool, trans, incoming, dataset_upload_inputs, history, preferred_object_store_id): + def _setup_job( + self, tool, trans: ProvidesHistoryContext, incoming, dataset_upload_inputs, history, preferred_object_store_id + ): check_timer = ExecutionTimer() uploaded_datasets = upload_common.get_uploaded_datasets( trans, "", incoming, dataset_upload_inputs, history=history @@ -105,7 +112,9 @@ class UploadToolAction(BaseUploadToolAction): class FetchUploadToolAction(BaseUploadToolAction): - def _setup_job(self, tool, trans, incoming, dataset_upload_inputs, history, preferred_object_store_id): + def _setup_job( + self, tool, trans: ProvidesHistoryContext, incoming, dataset_upload_inputs, history, preferred_object_store_id + ): # Now replace references in requests with these. files = incoming.get("files", []) files_iter = iter(files) @@ -135,7 +144,7 @@ class FetchUploadToolAction(BaseUploadToolAction): replace_file_srcs(request) - outputs = [] + outputs: list[HistoryDatasetAssociation | HistoryDatasetCollectionAssociation] = [] for target in request.get("targets", []): destination = target.get("destination") destination_type = destination.get("type") @@ -161,7 +170,7 @@ class FetchUploadToolAction(BaseUploadToolAction): ) -def _precreate_fetched_hdas(trans, history, target, outputs): +def _precreate_fetched_hdas(trans: ProvidesHistoryContext, history, target, outputs): for item in target.get("elements", []): name = item.get("name", None) if name is None: @@ -186,7 +195,7 @@ def _precreate_fetched_hdas(trans, history, target, outputs): item["object_id"] = data.id -def _precreate_fetched_collection_instance(trans, history, target, outputs): +def _precreate_fetched_collection_instance(trans: ProvidesHistoryContext, history, target, outputs): collection_type = target.get("collection_type") if not collection_type: # Can't precreate collections of unknown type at this time. diff --git a/lib/galaxy/tools/actions/upload_common.py b/lib/galaxy/tools/actions/upload_common.py index e24f9cb6627..219cc6f19a1 100644 --- a/lib/galaxy/tools/actions/upload_common.py +++ b/lib/galaxy/tools/actions/upload_common.py @@ -18,7 +18,11 @@ from galaxy.files.uris import ( stream_to_file, validate_non_local, ) -from galaxy.managers.context import ProvidesUserContext +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.model import ( DatasetPermissions, FormDefinition, @@ -43,7 +47,7 @@ def validate_datatype_extension(datatypes_registry, ext): raise RequestParameterInvalidException(f"Requested extension '{ext}' unknown, cannot upload dataset.") -def persist_uploads(params, trans): +def persist_uploads(params, trans: ProvidesAppContext): """ Turn any uploads in the submitted form to persisted files. """ @@ -87,7 +91,7 @@ class LibraryParams: def handle_library_params( - trans, params, folder_id: int, replace_dataset: LibraryDataset | None = None + trans: ProvidesAppContext, params, folder_id: int, replace_dataset: LibraryDataset | None = None ) -> LibraryParams: session = trans.sa_session # FIXME: the received params has already been parsed by util.Params() by the time it reaches here, @@ -125,7 +129,7 @@ def handle_library_params( ) -def __new_history_upload(trans, uploaded_dataset, history=None, state=None): +def __new_history_upload(trans: ProvidesHistoryContext, uploaded_dataset, history=None, state=None): if not history: history = trans.history hda = HistoryDatasetAssociation( @@ -143,12 +147,15 @@ def __new_history_upload(trans, uploaded_dataset, history=None, state=None): hda.state = hda.states.QUEUED history.add_dataset(hda, genome_build=uploaded_dataset.dbkey, quota=False) permissions = trans.app.security_agent.history_get_default_permissions(history) + assert hda.dataset is not None trans.app.security_agent.set_all_dataset_permissions(hda.dataset, permissions, new=True, flush=False) trans.sa_session.commit() return hda -def __new_library_upload(trans, cntrller, uploaded_dataset, library_bunch, tag_handler, state=None): +def __new_library_upload( + trans: ProvidesUserContext, cntrller, uploaded_dataset, library_bunch, tag_handler, state=None +): current_user_roles = trans.get_current_user_roles() if not ( (trans.user_is_admin and cntrller in ["library_admin", "api"]) @@ -213,6 +220,7 @@ def __new_library_upload(trans, cntrller, uploaded_dataset, library_bunch, tag_h ) else: # Copy the current user's DefaultUserPermissions to the new LibraryDatasetDatasetAssociation.dataset + assert ldda.dataset is not None trans.app.security_agent.set_all_dataset_permissions( ldda.dataset, trans.app.security_agent.user_get_default_permissions(trans.user), new=True ) @@ -252,7 +260,13 @@ def __new_library_upload(trans, cntrller, uploaded_dataset, library_bunch, tag_h def new_upload( - trans: ProvidesUserContext, cntrller, uploaded_dataset, library_bunch=None, history=None, state=None, tag_list=None + trans: ProvidesHistoryContext, + cntrller, + uploaded_dataset, + library_bunch=None, + history=None, + state=None, + tag_list=None, ): tag_handler = trans.tag_handler if library_bunch: @@ -280,7 +294,9 @@ def new_upload( return upload_target_dataset_instance -def get_uploaded_datasets(trans, cntrller, params, dataset_upload_inputs, library_bunch=None, history=None): +def get_uploaded_datasets( + trans: ProvidesHistoryContext, cntrller, params, dataset_upload_inputs, library_bunch=None, history=None +): uploaded_datasets = [] for dataset_upload_input in dataset_upload_inputs: uploaded_datasets.extend(dataset_upload_input.get_uploaded_datasets(trans, params)) @@ -290,7 +306,7 @@ def get_uploaded_datasets(trans, cntrller, params, dataset_upload_inputs, librar return uploaded_datasets -def create_paramfile(trans, uploaded_datasets): +def create_paramfile(trans: ProvidesUserContext, uploaded_datasets): """ Create the upload tool's JSON "param" file. """ @@ -333,7 +349,7 @@ def create_paramfile(trans, uploaded_datasets): except Exception: purge_source = True try: - user_ftp_dir = os.path.abspath(trans.user_ftp_dir) + user_ftp_dir = os.path.abspath(trans.user_ftp_dir) if trans.user_ftp_dir is not None else None except Exception: user_ftp_dir = None if user_ftp_dir and uploaded_dataset.path.startswith(user_ftp_dir): @@ -379,7 +395,7 @@ def create_paramfile(trans, uploaded_datasets): def create_job( - trans, + trans: ProvidesHistoryContext, params, tool, json_file_path, @@ -395,7 +411,7 @@ def create_job( job = Job() trans.sa_session.add(job) job.galaxy_version = trans.app.config.version_major - galaxy_session = trans.get_galaxy_session() + galaxy_session = trans.galaxy_session if isinstance(galaxy_session, GalaxySession): job.session_id = galaxy_session.id if trans.user is not None: @@ -438,7 +454,7 @@ def create_job( return job, output -def active_folders(trans, folder): +def active_folders(trans: ProvidesAppContext, folder): # Stolen from galaxy.web.controllers.library_common (importing from which causes a circular issues). # Much faster way of retrieving all active sub-folders within a given folder than the # performance of the mapper. This query also eagerloads the permissions on each folder. diff --git a/lib/galaxy/tools/error_reports/__init__.py b/lib/galaxy/tools/error_reports/__init__.py index 08def5f6c36..a3faf383651 100644 --- a/lib/galaxy/tools/error_reports/__init__.py +++ b/lib/galaxy/tools/error_reports/__init__.py @@ -8,6 +8,7 @@ from galaxy.exceptions import ( ItemAccessibilityException, UserRequiredException, ) +from galaxy.managers.context import ProvidesUserContext from galaxy.util import plugin_config log = logging.getLogger(__name__) @@ -64,7 +65,7 @@ class ErrorPlugin: roles = [] return self.app.security_agent.can_access_dataset(roles, dataset.dataset) - def _check_invocation_accessibility(self, trans, invocation, user): + def _check_invocation_accessibility(self, trans: ProvidesUserContext | None, invocation, user): if not user: raise UserRequiredException("User is not logged in", type="error") if not trans or not self.app.workflow_manager.check_security(trans, invocation, check_ownership=False): diff --git a/lib/galaxy/tools/execute.py b/lib/galaxy/tools/execute.py index 38f4e3caaa2..a4cacd95525 100644 --- a/lib/galaxy/tools/execute.py +++ b/lib/galaxy/tools/execute.py @@ -49,13 +49,13 @@ from galaxy.tools.execution_helpers import ( ) from galaxy.tools.parameters.workflow_utils import is_runtime_value from galaxy.util.json import swap_inf_nan -from galaxy.work.context import WorkRequestContext from ._types import ( ToolRequestT, ToolStateJobInstancePopulatedT, ) if typing.TYPE_CHECKING: + from galaxy.managers.context import ProvidesHistoryContext from galaxy.tools import Tool log = logging.getLogger(__name__) @@ -96,7 +96,7 @@ class MappingParameters(NamedTuple): assert self.validated_param_template is not None assert self.validated_param_combinations is not None - def example_params(self, trans: WorkRequestContext) -> ToolStateJobInstancePopulatedT: + def example_params(self, trans: "ProvidesHistoryContext") -> ToolStateJobInstancePopulatedT: """Representative per-job params for output-structure determination. Normally returns ``param_combinations[0]``. When the request @@ -113,11 +113,11 @@ class MappingParameters(NamedTuple): return _resolve_template(self.param_template, trans) -def _resolve_template(template: ToolRequestT, trans: WorkRequestContext) -> ToolStateJobInstancePopulatedT: +def _resolve_template(template: ToolRequestT, trans: "ProvidesHistoryContext") -> ToolStateJobInstancePopulatedT: return {key: _resolve_template_value(value, trans) for key, value in template.items()} -def _resolve_template_value(value: Any, trans: WorkRequestContext) -> Any: +def _resolve_template_value(value: Any, trans: "ProvidesHistoryContext") -> Any: if isinstance(value, dict): values = value.get("values") if ( @@ -138,7 +138,7 @@ def _resolve_template_value(value: Any, trans: WorkRequestContext) -> Any: def _resolve_collection_ref( ref: dict[str, Any], - trans: WorkRequestContext, + trans: "ProvidesHistoryContext", raw_fallback: Any, ) -> model.HistoryDatasetCollectionAssociation | model.DatasetCollectionElement | Any: src = ref.get("src") @@ -163,7 +163,7 @@ def _resolve_collection_ref( def execute_async( - trans, + trans: "ProvidesHistoryContext", tool: "Tool", mapping_params: MappingParameters, history: model.History, @@ -204,7 +204,7 @@ def execute_async( def execute( - trans, + trans: "ProvidesHistoryContext", tool: "Tool", mapping_params: MappingParameters, history: model.History, @@ -247,7 +247,7 @@ def execute( def _execute( - trans, + trans: "ProvidesHistoryContext", tool: "Tool", mapping_params: MappingParameters, history: model.History, @@ -465,7 +465,7 @@ class ExecutionTracker: def __init__( self, - trans, + trans: "ProvidesHistoryContext", tool: "Tool", mapping_params: MappingParameters, collection_info: MatchingCollections | None, @@ -555,7 +555,7 @@ class ExecutionTracker: ) return self._on_text - def output_name(self, trans, history, params, output): + def output_name(self, trans: "ProvidesHistoryContext", history, params, output): on_text = self.on_text try: @@ -619,7 +619,7 @@ class ExecutionTracker: leaf_subcollection_type=subcollection_mapping_type, ) - def _structure_for_output(self, trans, tool_output): + def _structure_for_output(self, trans: "ProvidesHistoryContext", tool_output): collection_info = self.collection_info assert collection_info structure = collection_info.structure @@ -640,7 +640,7 @@ class ExecutionTracker: return structure - def _mapped_output_structure(self, trans, tool_output): + def _mapped_output_structure(self, trans: "ProvidesHistoryContext", tool_output): collections_manager = trans.app.dataset_collection_manager output_structure = tool_output_to_structure( self.sliced_input_collection_structure, tool_output, collections_manager @@ -727,7 +727,7 @@ class ExecutionTracker: else: return None - def finalize_dataset_collections(self, trans): + def finalize_dataset_collections(self, trans: "ProvidesHistoryContext"): # TODO: this probably needs to be reworked some, we should have the collection methods # return a list of changed objects to add to the session and flush and we should only # be finalizing collections to a depth of self.collection_info.structure. So for instance @@ -848,7 +848,7 @@ class ExecutionTracker: class ToolExecutionTracker(ExecutionTracker): def __init__( self, - trans, + trans: "ProvidesHistoryContext", tool: "Tool", mapping_params: MappingParameters, collection_info: MatchingCollections | None, @@ -893,7 +893,7 @@ class ToolExecutionTracker(ExecutionTracker): class WorkflowStepExecutionTracker(ExecutionTracker): def __init__( self, - trans, + trans: "ProvidesHistoryContext", tool: "Tool", mapping_params: MappingParameters, collection_info: MatchingCollections | None, diff --git a/lib/galaxy/tools/execution_helpers.py b/lib/galaxy/tools/execution_helpers.py index 7f1d24ecda8..b22282f07c5 100644 --- a/lib/galaxy/tools/execution_helpers.py +++ b/lib/galaxy/tools/execution_helpers.py @@ -5,9 +5,12 @@ tool execution code, and tool action code. """ import logging +from typing import Any from more_itertools import consecutive_groups +from galaxy.managers.context import ProvidesUserContext + log = logging.getLogger(__name__) @@ -16,11 +19,11 @@ class ToolExecutionCache: the same tool by the same user with slightly different parameters. """ - def __init__(self, trans): + def __init__(self, trans: ProvidesUserContext): self.trans = trans self.current_user_roles = trans.get_current_user_roles() - self.chrom_info = {} - self.cached_collection_elements = {} + self.chrom_info: dict[str, Any] = {} + self.cached_collection_elements: dict[Any, Any] = {} def get_chrom_info(self, tool_id, input_dbkey): genome_builds = self.trans.app.genome_builds diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index e518e2a454c..01e3214bafb 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -132,7 +132,7 @@ def contains_workflow_parameter(value, search=False): return False -def is_runtime_context(trans, other_values): +def is_runtime_context(trans: "ProvidesHistoryContext", other_values): if trans.workflow_building_mode: return True for context_value in other_values.values(): @@ -241,14 +241,14 @@ class ToolParameter(UsesDictVisibleKeys): """Return user friendly name for the parameter""" return self.label if self.label else self.name - def from_json(self, value, trans, other_values=None): + def from_json(self, value, trans: "ProvidesHistoryContext", other_values=None): """ Convert a value from an HTML POST into the parameters preferred value format. """ return value - def get_initial_value(self, trans, other_values): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): """ Return the starting value of the parameter """ @@ -346,7 +346,7 @@ class ToolParameter(UsesDictVisibleKeys): value = sanitize_param(value) return value - def validate(self, value, trans=None) -> None: + def validate(self, value, trans: "ProvidesHistoryContext | None" = None) -> None: if value in ["", None] and self.optional: return for validator in self.validators: @@ -355,7 +355,7 @@ class ToolParameter(UsesDictVisibleKeys): except ValueError as e: raise ParameterValueError(str(e), self.name, value) from None - def to_dict(self, trans, other_values=None): + def to_dict(self, trans: "ProvidesHistoryContext", other_values=None): """to_dict tool parameter. This can be overridden by subclasses.""" other_values = other_values or {} tool_dict = self._dictify_view_keys() @@ -402,7 +402,7 @@ class SimpleTextToolParameter(ToolParameter): else: self.value = "" - def get_initial_value(self, trans, other_values): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): return self.value @@ -436,7 +436,7 @@ class TextToolParameter(SimpleTextToolParameter): self.value = input_source.get("value") self.area = input_source.get_bool("area", False) - def validate(self, value, trans=None): + def validate(self, value, trans: "ProvidesHistoryContext | None" = None): search = self.type == "text" if not ( trans @@ -455,7 +455,7 @@ class TextToolParameter(SimpleTextToolParameter): default_value = "" return default_value - def to_dict(self, trans, other_values=None): + def to_dict(self, trans: "ProvidesHistoryContext", other_values=None): d = super().to_dict(trans) other_values = other_values or {} d["area"] = self.area @@ -504,7 +504,7 @@ class IntegerToolParameter(TextToolParameter): if self.min is not None or self.max is not None: self.validators.append(validation.InRangeValidator.simple_range_validator(self.min, self.max)) - def from_json(self, value, trans, other_values=None): + def from_json(self, value, trans: "ProvidesHistoryContext", other_values=None): other_values = other_values or {} try: return int(value) @@ -530,7 +530,7 @@ class IntegerToolParameter(TextToolParameter): return None raise ParameterValueError("an integer is required", self.name, value) - def get_initial_value(self, trans, other_values): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): if self.value is not None and self.value != "": return int(self.value) else: @@ -577,7 +577,7 @@ class FloatToolParameter(TextToolParameter): if self.min is not None or self.max is not None: self.validators.append(validation.InRangeValidator.simple_range_validator(self.min, self.max)) - def from_json(self, value, trans, other_values=None): + def from_json(self, value, trans: "ProvidesHistoryContext", other_values=None): other_values = other_values or {} try: return float(value) @@ -603,7 +603,7 @@ class FloatToolParameter(TextToolParameter): return None raise ParameterValueError("a float is required", self.name, value) - def get_initial_value(self, trans, other_values): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): if self.value is None: return None try: @@ -652,7 +652,7 @@ class BooleanToolParameter(ToolParameter): self.optional = input_source.get_bool("optional", False) self.checked = boolean_is_checked(input_source) - def from_json(self, value, trans, other_values=None): + def from_json(self, value, trans: "ProvidesHistoryContext", other_values=None): return self.to_python(value) def to_python(self, value, app=None): @@ -665,7 +665,7 @@ class BooleanToolParameter(ToolParameter): def to_json(self, value, app, use_security): return self.to_python(value, app) - def get_initial_value(self, trans, other_values): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): return self.checked def to_param_dict_string(self, value, other_values=None): @@ -674,7 +674,7 @@ class BooleanToolParameter(ToolParameter): else: return self.falsevalue - def to_dict(self, trans, other_values=None): + def to_dict(self, trans: "ProvidesHistoryContext", other_values=None): d = super().to_dict(trans) d["truevalue"] = self.truevalue d["falsevalue"] = self.falsevalue @@ -700,7 +700,7 @@ class FileToolParameter(ToolParameter): [('argument', None), ('help', ''), ('help_format', 'html'), ('hidden', False), ('is_dynamic', False), ('label', ''), ('model_class', 'FileToolParameter'), ('name', '_name'), ('optional', False), ('refresh_on_change', False), ('type', 'file'), ('value', None)] """ - def from_json(self, value, trans, other_values=None): + def from_json(self, value, trans: "ProvidesHistoryContext", other_values=None): # Middleware or proxies may encode files in special ways (TODO: this # should be pluggable) if isinstance(value, FilesPayload): @@ -781,7 +781,7 @@ class FTPFileToolParameter(ToolParameter): self.optional = input_source.parse_optional(True) self.user_ftp_dir = "" - def get_initial_value(self, trans, other_values): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): if trans is not None: if trans.user is not None: self.user_ftp_dir = f"{trans.user_ftp_dir}/" @@ -803,7 +803,7 @@ class FTPFileToolParameter(ToolParameter): else: return lst[0] - def from_json(self, value, trans, other_values=None): + def from_json(self, value, trans: "ProvidesHistoryContext", other_values=None): return self.to_python(value, trans.app, validate=True) def to_json(self, value, app, use_security): @@ -831,7 +831,7 @@ class FTPFileToolParameter(ToolParameter): raise ValueError("The FTP directory is not configured.") return lst - def to_dict(self, trans, other_values=None): + def to_dict(self, trans: "ProvidesHistoryContext", other_values=None): d = super().to_dict(trans) d["multiple"] = self.multiple return d @@ -855,7 +855,7 @@ class HiddenToolParameter(ToolParameter): self.value = input_source.get("value") self.hidden = True - def get_initial_value(self, trans, other_values): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): return self.value def get_label(self): @@ -891,7 +891,7 @@ class ColorToolParameter(ToolParameter): self.value = get_color_value(input_source) self.rgb = input_source.get_bool("rgb", False) - def get_initial_value(self, trans, other_values): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): if self.value is not None: return self.value.lower() @@ -922,22 +922,25 @@ class BaseURLToolParameter(HiddenToolParameter): super().__init__(tool, input_source) self.value = input_source.get("value", "") - def get_initial_value(self, trans, other_values): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): + if trans is None: + return self.value return self._get_value(trans) - def from_json(self, value, trans, other_values=None): + def from_json(self, value, trans: "ProvidesHistoryContext", other_values=None): return self._get_value(trans) - def _get_value(self, trans): + def _get_value(self, trans: "ProvidesHistoryContext"): try: if not self.value.startswith("/"): raise Exception("baseurl value must start with a /") + assert trans.url_builder is not None return trans.url_builder(self.value, qualified=True) except Exception as e: log.debug('Url creation failed for "%s": %s', self.name, unicodify(e)) return self.value - def to_dict(self, trans, other_values=None): + def to_dict(self, trans: "ProvidesHistoryContext", other_values=None): d = super().to_dict(trans) return d @@ -1004,14 +1007,16 @@ class SelectToolParameter(ToolParameter): self.legal_values.add(value) self.is_dynamic = (self.dynamic_options is not None) or (self.options is not None) - def _get_dynamic_options_call_other_values(self, trans, other_values): + def _get_dynamic_options_call_other_values(self, trans: "ProvidesHistoryContext", other_values): call_other_values = ExpressionContext({"__trans__": trans}) if other_values: call_other_values.parent = other_values.parent call_other_values.update(other_values.dict) return call_other_values - def get_options(self, trans, other_values) -> Sequence[ParameterOption | DrillDownOptionsDict]: + def get_options( + self, trans: "ProvidesHistoryContext", other_values + ) -> Sequence[ParameterOption | DrillDownOptionsDict]: if self.options: return self.options.get_options(trans, other_values) elif self.dynamic_options: @@ -1032,24 +1037,24 @@ class SelectToolParameter(ToolParameter): else: return [ParameterOption(*o) for o in self.static_options] - def get_legal_values(self, trans, other_values, value): + def get_legal_values(self, trans: "ProvidesHistoryContext", other_values, value): """ determine the set of values of legal options """ options = cast(list[ParameterOption], self.get_options(trans, other_values)) return {option.dataset or option.value for option in options} - def get_legal_names(self, trans, other_values): + def get_legal_names(self, trans: "ProvidesHistoryContext", other_values): """ determine the set of values of legal options """ options = cast(list[ParameterOption], self.get_options(trans, other_values)) return {option.name: option.value for option in options} - def from_json(self, value, trans, other_values=None): + def from_json(self, value, trans: "ProvidesHistoryContext", other_values=None): return self._select_from_json(value, trans, other_values=other_values, require_legal_value=True) - def _select_from_json(self, value, trans, other_values=None, require_legal_value=True): + def _select_from_json(self, value, trans: "ProvidesHistoryContext", other_values=None, require_legal_value=True): other_values = other_values or {} try: legal_values = self.get_legal_values(trans, other_values, value) @@ -1178,11 +1183,18 @@ class SelectToolParameter(ToolParameter): return history_item_dict_to_python(value, app, self.name) return super().to_python(value, app) - def get_initial_value(self, trans, other_values): - try: - options = cast(list[ParameterOption], self.get_options(trans, other_values)) - except ImplicitConversionRequired: - return None + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): + options: list[ParameterOption] + if trans is None: + # Conditional case inference walks tool state without a transaction + # (see galaxy.tools.parameters.visit_input_values); only statically + # declared options can be resolved without one. + options = [ParameterOption(*o) for o in self.static_options] + else: + try: + options = cast(list[ParameterOption], self.get_options(trans, other_values)) + except ImplicitConversionRequired: + return None if not options: return None value = [option.value for option in options if option.selected] @@ -1225,7 +1237,7 @@ class SelectToolParameter(ToolParameter): else: return [] - def to_dict(self, trans, other_values=None): + def to_dict(self, trans: "ProvidesHistoryContext", other_values=None): other_values = other_values or {} d = super().to_dict(trans, other_values) @@ -1237,7 +1249,7 @@ class SelectToolParameter(ToolParameter): d["textable"] = is_runtime_context(trans, other_values) return d - def validate(self, value, trans=None): + def validate(self, value, trans: "ProvidesHistoryContext | None" = None): if not value: super().validate(value, trans) if self.multiple: @@ -1276,7 +1288,7 @@ class GenomeBuildParameter(SelectToolParameter): self.static_options = [(value, key, False) for key, value in self._get_dbkey_names()] self.is_dynamic = True - def get_options(self, trans, other_values) -> Sequence[ParameterOption]: + def get_options(self, trans: "ProvidesHistoryContext", other_values) -> Sequence[ParameterOption]: last_used_build = object() if trans.history: last_used_build = trans.history.genome_build @@ -1285,10 +1297,10 @@ class GenomeBuildParameter(SelectToolParameter): for dbkey, build_name in self._get_dbkey_names(trans=trans) ] - def get_legal_values(self, trans, other_values, value): + def get_legal_values(self, trans: "ProvidesHistoryContext", other_values, value): return {dbkey for dbkey, _ in self._get_dbkey_names(trans=trans)} - def to_dict(self, trans, other_values=None): + def to_dict(self, trans: "ProvidesHistoryContext", other_values=None): # skip SelectToolParameter (the immediate parent) bc we need to get options in a different way here d = ToolParameter.to_dict(self, trans) @@ -1302,7 +1314,7 @@ class GenomeBuildParameter(SelectToolParameter): d.update( { - "options": serialize_options(trans, options), + "options": serialize_options(trans.security, options), "value": value, "display": self.display, "multiple": self.multiple, @@ -1311,7 +1323,7 @@ class GenomeBuildParameter(SelectToolParameter): return d - def _get_dbkey_names(self, trans=None): + def _get_dbkey_names(self, trans: "ProvidesHistoryContext | None" = None): if not self.tool: # Hack for unit tests, since we have no tool return read_dbnames(None) @@ -1341,7 +1353,7 @@ class SelectTagParameter(SelectToolParameter): self.default_value = input_source.get("value", None) self.is_dynamic = True - def from_json(self, value, trans, other_values=None): + def from_json(self, value, trans: "ProvidesHistoryContext", other_values=None): other_values = other_values or {} if self.multiple: tag_list = [] @@ -1384,7 +1396,7 @@ class SelectTagParameter(SelectToolParameter): tags.add(tag.user_value) return list(tags) - def get_options(self, trans, other_values) -> Sequence[ParameterOption]: + def get_options(self, trans: "ProvidesHistoryContext", other_values) -> Sequence[ParameterOption]: """ Show tags """ @@ -1393,12 +1405,12 @@ class SelectTagParameter(SelectToolParameter): options.append(ParameterOption(f"Tags: {tag}", tag, False)) return options - def get_initial_value(self, trans, other_values): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): if self.default_value is not None: return self.default_value return super().get_initial_value(trans, other_values) - def get_legal_values(self, trans, other_values, value): + def get_legal_values(self, trans: "ProvidesHistoryContext", other_values, value): if self.data_ref not in other_values and not trans.workflow_building_mode: raise ValueError("Value for associated data reference not found (data_ref).") return set(self.get_tag_list(other_values)) @@ -1406,7 +1418,7 @@ class SelectTagParameter(SelectToolParameter): def get_dependencies(self): return [self.data_ref] - def to_dict(self, trans, other_values=None): + def to_dict(self, trans: "ProvidesHistoryContext", other_values=None): other_values = other_values or {} d = super().to_dict(trans, other_values=other_values) d["data_ref"] = self.data_ref @@ -1467,7 +1479,7 @@ class ColumnListParameter(SelectToolParameter): return value.strip() return value - def from_json(self, value, trans, other_values=None): + def from_json(self, value, trans: "ProvidesHistoryContext", other_values=None): """ Label convention prepends column number with a 'c', but tool uses the integer. This removes the 'c' when entered into a workflow. @@ -1500,7 +1512,7 @@ class ColumnListParameter(SelectToolParameter): column = column.lower()[1:] return column - def get_column_list(self, trans, other_values): + def get_column_list(self, trans: "ProvidesHistoryContext", other_values): """ Generate a select list containing the columns of the associated dataset (if found). @@ -1545,7 +1557,7 @@ class ColumnListParameter(SelectToolParameter): column_list = [c for c in column_list if c in this_column_list] return column_list - def get_options(self, trans, other_values) -> Sequence[ParameterOption]: + def get_options(self, trans: "ProvidesHistoryContext", other_values) -> Sequence[ParameterOption]: """ Show column labels rather than c1..cn if use_header_names=True """ @@ -1585,12 +1597,12 @@ class ColumnListParameter(SelectToolParameter): options = [ParameterOption(f"Column: {col}", col, False) for col in column_list] return options - def get_initial_value(self, trans, other_values): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): if self.default_value is not None: return self.default_value return super().get_initial_value(trans, other_values) - def get_legal_values(self, trans, other_values, value): + def get_legal_values(self, trans: "ProvidesHistoryContext", other_values, value): if self.data_ref not in other_values: raise ValueError("Value for associated data reference not found (data_ref).") legal_values = self.get_column_list(trans, other_values) @@ -1604,7 +1616,7 @@ class ColumnListParameter(SelectToolParameter): return set(legal_values) - def is_file_empty(self, trans, other_values): + def is_file_empty(self, trans: "ProvidesHistoryContext", other_values): for dataset in util.listify(other_values.get(self.data_ref)): # Use representative dataset if a dataset collection is parsed if isinstance(dataset, HistoryDatasetCollectionAssociation): @@ -1628,7 +1640,7 @@ class ColumnListParameter(SelectToolParameter): def get_dependencies(self): return [self.data_ref] - def to_dict(self, trans, other_values=None): + def to_dict(self, trans: "ProvidesHistoryContext", other_values=None): other_values = other_values or {} d = super().to_dict(trans, other_values=other_values) d["data_ref"] = self.data_ref @@ -1701,7 +1713,7 @@ class DrillDownSelectToolParameter(SelectToolParameter): self.dynamic_options = None self.options = input_source.parse_drill_down_static_options(tool_data_path) - def _get_options_from_code(self, trans=None, other_values=None): + def _get_options_from_code(self, trans: "ProvidesHistoryContext | None" = None, other_values=None): assert self.dynamic_options, Exception("dynamic_options was not specifed") call_other_values = ExpressionContext({"__trans__": trans, "__value__": None}) if other_values: @@ -1713,7 +1725,9 @@ class DrillDownSelectToolParameter(SelectToolParameter): except Exception: return [] - def get_options(self, trans=None, other_values=None) -> list[DrillDownOptionsDict]: + def get_options( + self, trans: "ProvidesHistoryContext | None" = None, other_values=None + ) -> list[DrillDownOptionsDict]: other_values = other_values or {} if self.is_dynamic: if self.dynamic_options: @@ -1722,7 +1736,7 @@ class DrillDownSelectToolParameter(SelectToolParameter): return self.options - def get_legal_values(self, trans, other_values, value): + def get_legal_values(self, trans: "ProvidesHistoryContext", other_values, value): def recurse_options(legal_values, options: list[DrillDownOptionsDict]): for option in options: legal_values.append(option["value"]) @@ -1732,7 +1746,7 @@ class DrillDownSelectToolParameter(SelectToolParameter): recurse_options(legal_values, self.get_options(trans=trans, other_values=other_values)) return legal_values - def from_json(self, value, trans, other_values=None): + def from_json(self, value, trans: "ProvidesHistoryContext", other_values=None): other_values = other_values or {} legal_values = self.get_legal_values(trans, other_values, value) if not legal_values and trans.workflow_building_mode: @@ -1813,7 +1827,7 @@ class DrillDownSelectToolParameter(SelectToolParameter): rval = sanitize_param(rval) return rval - def get_initial_value(self, trans, other_values): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): def recurse_options(initial_values, options: list[DrillDownOptionsDict]): for option in options: if option["selected"]: @@ -1865,7 +1879,7 @@ class DrillDownSelectToolParameter(SelectToolParameter): def get_dependencies(self): return [] - def to_dict(self, trans, other_values=None): + def to_dict(self, trans: "ProvidesHistoryContext", other_values=None): other_values = other_values or {} # skip SelectToolParameter (the immediate parent) bc we need to get options in a different way here d = ToolParameter.to_dict(self, trans) @@ -1961,7 +1975,7 @@ class BaseDataToolParameter(ToolParameter): # with None" (which is a legitimate return for parameters with no formats). _ACCEPTABLE_EXTENSIONS_UNSET: Any = object() - def __init__(self, tool: Optional["Tool"], input_source, trans): + def __init__(self, tool: Optional["Tool"], input_source, trans: "ProvidesHistoryContext | None"): super().__init__(tool, input_source) self.min = input_source.get("min") self.max = input_source.get("max") @@ -1993,7 +2007,7 @@ class BaseDataToolParameter(ToolParameter): self.tool.app.datatypes_registry ) # can be None if self.tool.app is a ValidationContext - def _parse_formats(self, trans, input_source): + def _parse_formats(self, trans: "ProvidesHistoryContext | None", input_source): """ Build list of classes for supported data formats """ @@ -2058,7 +2072,8 @@ class BaseDataToolParameter(ToolParameter): return True return False - def get_initial_value(self, trans, other_values): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", other_values): + assert trans is not None if trans.workflow_building_mode is workflow_building_modes.ENABLED or trans.app.name == "tool_shed": return RuntimeValue() if self.optional: @@ -2157,7 +2172,7 @@ class BaseDataToolParameter(ToolParameter): else: return app.model.context.get(HistoryDatasetAssociation, int(value)) - def validate(self, value, trans=None): + def validate(self, value, trans: "ProvidesHistoryContext | None" = None): def do_validate(v): for validator in self.validators: if ( @@ -2297,7 +2312,7 @@ class DataToolParameter(BaseDataToolParameter): security stuff will dramatically alter this anyway. """ - def __init__(self, tool: Optional["Tool"], input_source, trans=None): + def __init__(self, tool: Optional["Tool"], input_source, trans: "ProvidesHistoryContext | None" = None): input_source = ensure_input_source(input_source) super().__init__(tool, input_source, trans) self.load_contents = int(input_source.get("load_contents", 0)) @@ -2348,7 +2363,7 @@ class DataToolParameter(BaseDataToolParameter): allow_uri_if_protocol = input_source.get("allow_uri_if_protocol", None) self.allow_uri_if_protocol = allow_uri_if_protocol.split(",") if allow_uri_if_protocol else [] - def from_json(self, value, trans, other_values=None): + def from_json(self, value, trans: "ProvidesHistoryContext", other_values=None): session = trans.sa_session other_values = other_values or {} @@ -2358,6 +2373,7 @@ class DataToolParameter(BaseDataToolParameter): raise ParameterValueError("specify a dataset of the required format / build for parameter", self.name) if value in [None, "None", ""]: if self.default_object: + assert trans.history is not None return raw_to_galaxy(trans.app, trans.history, self.default_object) return None batch_wrapper = False @@ -2529,7 +2545,7 @@ class DataToolParameter(BaseDataToolParameter): else: return [] - def converter_safe(self, other_values, trans): + def converter_safe(self, other_values, trans: "ProvidesHistoryContext"): if ( self.tool is None or self.tool.has_multiple_pages @@ -2574,7 +2590,9 @@ class DataToolParameter(BaseDataToolParameter): ref = ref() return str(ref) - def to_dict(self, trans, other_values=None, pagination: ParameterPaginationT | None = None): + def to_dict( + self, trans: "ProvidesHistoryContext", other_values=None, pagination: ParameterPaginationT | None = None + ): other_values = other_values or {} d = super().to_dict(trans) self._fill_to_dict_static(d) @@ -2793,7 +2811,7 @@ class DataToolParameter(BaseDataToolParameter): make_hdca_entry(builder.security, hdca, name, keep=False, subcollection_type=subcollection_type) ) - def _history_query(self, trans): + def _history_query(self, trans: "ProvidesHistoryContext"): assert self.multiple dataset_collection_type_descriptions = trans.app.dataset_collection_manager.collection_type_descriptions # If multiple data parameter, treat like a list parameter. @@ -2803,7 +2821,7 @@ class DataToolParameter(BaseDataToolParameter): class DataCollectionToolParameter(BaseDataToolParameter): """ """ - def __init__(self, tool: Optional["Tool"], input_source, trans=None): + def __init__(self, tool: Optional["Tool"], input_source, trans: "ProvidesHistoryContext | None" = None): input_source = ensure_input_source(input_source) super().__init__(tool, input_source, trans) self._parse_formats(trans, input_source) @@ -2830,11 +2848,11 @@ class DataCollectionToolParameter(BaseDataToolParameter): def collection_types(self) -> list[str] | None: return self._collection_types - def _history_query(self, trans): + def _history_query(self, trans: "ProvidesHistoryContext"): dataset_collection_type_descriptions = trans.app.dataset_collection_manager.collection_type_descriptions return query.HistoryQuery.from_parameter(self, dataset_collection_type_descriptions) - def match_collections(self, trans, history, dataset_collection_matcher): + def match_collections(self, trans: "ProvidesHistoryContext", history, dataset_collection_matcher): dataset_collections = trans.app.dataset_collection_manager.history_dataset_collections( history, self._history_query(trans) ) @@ -2850,7 +2868,7 @@ class DataCollectionToolParameter(BaseDataToolParameter): continue yield dataset_collection_instance, match.implicit_conversion - def match_multirun_collections(self, trans, history, dataset_collection_matcher): + def match_multirun_collections(self, trans: "ProvidesHistoryContext", history, dataset_collection_matcher): for history_dataset_collection in history.active_visible_dataset_collections: if not self._history_query(trans).can_map_over(history_dataset_collection): continue @@ -2859,7 +2877,7 @@ class DataCollectionToolParameter(BaseDataToolParameter): if match: yield history_dataset_collection, match.implicit_conversion - def from_json(self, value, trans, other_values=None): + def from_json(self, value, trans: "ProvidesHistoryContext", other_values=None): session = trans.sa_session other_values = other_values or {} @@ -2870,6 +2888,7 @@ class DataCollectionToolParameter(BaseDataToolParameter): raise ParameterValueError("specify a dataset collection of the correct type", self.name) if value in [None, "None"]: if self.default_object: + assert trans.history is not None return raw_to_galaxy(trans.app, trans.history, self.default_object) return None if isinstance(value, MutableMapping) and "values" in value: @@ -2933,7 +2952,9 @@ class DataCollectionToolParameter(BaseDataToolParameter): display_text = "No dataset collection." return display_text - def to_dict(self, trans, other_values=None, pagination: ParameterPaginationT | None = None): + def to_dict( + self, trans: "ProvidesHistoryContext", other_values=None, pagination: ParameterPaginationT | None = None + ): other_values = other_values or {} d = super().to_dict(trans) d["collection_types"] = self.collection_types @@ -3098,10 +3119,11 @@ class BaseJsonToolParameter(ToolParameter): class DirectoryUriToolParameter(SimpleTextToolParameter): """galaxy.files URIs for directories.""" - def validate(self, value, trans=None): + def validate(self, value, trans: "ProvidesHistoryContext | None" = None): super().validate(value, trans=trans) if not value: return # value is not set yet, do not validate + assert trans is not None # Skip file source validation in workflow building mode to allow workflows # referencing removed file sources to be exported/viewed. Users can then # download and edit them. Validation still occurs during tool execution. @@ -3135,7 +3157,7 @@ class RulesListToolParameter(BaseJsonToolParameter): super().__init__(tool, input_source) self.data_ref = input_source.get("data_ref", None) - def to_dict(self, trans, other_values=None): + def to_dict(self, trans: "ProvidesHistoryContext", other_values=None): other_values = other_values or {} d = ToolParameter.to_dict(self, trans) if target := other_values.get(self.data_ref): @@ -3146,7 +3168,7 @@ class RulesListToolParameter(BaseJsonToolParameter): } return d - def validate(self, value, trans=None): + def validate(self, value, trans: "ProvidesHistoryContext | None" = None): super().validate(value, trans=trans) if not isinstance(value, MutableMapping): raise ValueError("No rules specified for rules parameter.") diff --git a/lib/galaxy/tools/parameters/dataset_matcher.py b/lib/galaxy/tools/parameters/dataset_matcher.py index 9a079c41aaf..dc634a800ed 100644 --- a/lib/galaxy/tools/parameters/dataset_matcher.py +++ b/lib/galaxy/tools/parameters/dataset_matcher.py @@ -1,19 +1,26 @@ from logging import getLogger +from typing import ( + Any, + TYPE_CHECKING, +) import galaxy.model +if TYPE_CHECKING: + from galaxy.managers.context import ProvidesHistoryContext + log = getLogger(__name__) -def set_dataset_matcher_factory(trans, tool): +def set_dataset_matcher_factory(trans: "ProvidesHistoryContext", tool): trans.dataset_matcher_factory = DatasetMatcherFactory(trans, tool) -def unset_dataset_matcher_factory(trans): +def unset_dataset_matcher_factory(trans: "ProvidesHistoryContext"): trans.dataset_matcher_factory = None -def get_dataset_matcher_factory(trans): +def get_dataset_matcher_factory(trans: "ProvidesHistoryContext"): dataset_matcher_factory = getattr(trans, "dataset_matcher_factory", None) return dataset_matcher_factory or DatasetMatcherFactory(trans) @@ -21,11 +28,11 @@ def get_dataset_matcher_factory(trans): class DatasetMatcherFactory: """""" - def __init__(self, trans, tool=None): + def __init__(self, trans: "ProvidesHistoryContext", tool=None): self._trans = trans self._tool = tool - self._data_inputs = [] - self._matches_format_cache = {} + self._data_inputs: list[Any] = [] + self._matches_format_cache: dict[str, dict[str, bool]] = {} if tool: valid_input_states = tool.valid_input_states else: @@ -96,7 +103,7 @@ class DatasetMatcher: and permission handling. """ - def __init__(self, dataset_matcher_factory, trans, param, other_values): + def __init__(self, dataset_matcher_factory, trans: "ProvidesHistoryContext", param, other_values): self.dataset_matcher_factory = dataset_matcher_factory self.trans = trans self.param = param @@ -202,7 +209,7 @@ class HdcaImplicitMatch: class SummaryDatasetCollectionMatcher: - def __init__(self, dataset_matcher_factory, trans, dataset_matcher): + def __init__(self, dataset_matcher_factory, trans: "ProvidesHistoryContext", dataset_matcher): self.dataset_matcher_factory = dataset_matcher_factory self._trans = trans self.dataset_matcher = dataset_matcher @@ -238,7 +245,7 @@ class SummaryDatasetCollectionMatcher: class DatasetCollectionMatcher: - def __init__(self, trans, dataset_matcher): + def __init__(self, trans: "ProvidesHistoryContext", dataset_matcher): self.dataset_matcher = dataset_matcher self._trans = trans diff --git a/lib/galaxy/tools/parameters/dynamic_options.py b/lib/galaxy/tools/parameters/dynamic_options.py index 61d7e1e0016..65a6437c030 100644 --- a/lib/galaxy/tools/parameters/dynamic_options.py +++ b/lib/galaxy/tools/parameters/dynamic_options.py @@ -18,6 +18,7 @@ from typing import ( Literal, ) +from galaxy.managers.context import ProvidesHistoryContext from galaxy.model import ( DatasetCollectionElement, HistoryDatasetAssociation, @@ -39,7 +40,6 @@ from galaxy.util import ( string_as_bool, ) from galaxy.util.template import fill_template -from galaxy.work.context import WorkRequestContext from . import validation from .cancelable_request import request @@ -66,7 +66,7 @@ class Filter: """Returns the name of any dependencies, otherwise None""" return None - def filter_options(self, options, trans, other_values): + def filter_options(self, options, trans: ProvidesHistoryContext, other_values): """Returns a list of options after the filter is applied""" raise TypeError("Abstract Method") @@ -94,7 +94,7 @@ class StaticValueFilter(Filter): self.column = d_option.column_spec_to_index(column) self.keep = string_as_bool(elem.get("keep", "True")) - def filter_options(self, options: Sequence[ParameterOption], trans, other_values): + def filter_options(self, options: Sequence[ParameterOption], trans: ProvidesHistoryContext, other_values): rval = [] filter_value = self.value try: @@ -130,7 +130,7 @@ class RegexpFilter(Filter): self.column = d_option.column_spec_to_index(column) self.keep = string_as_bool(elem.get("keep", "True")) - def filter_options(self, options: Sequence[ParameterOption], trans, other_values): + def filter_options(self, options: Sequence[ParameterOption], trans: ProvidesHistoryContext, other_values): rval = [] filter_value = self.value try: @@ -186,7 +186,7 @@ class DataMetaFilter(Filter): def get_dependency_name(self): return self.ref_name - def filter_options(self, options: Sequence[ParameterOption], trans: WorkRequestContext | None, other_values): + def filter_options(self, options: Sequence[ParameterOption], trans: ProvidesHistoryContext, other_values): options = list(options) if trans and trans.workflow_building_mode is workflow_building_modes.USE_HISTORY: # We're in the run form, can't possibly apply a data_meta filter. @@ -295,7 +295,7 @@ class ParamValueFilter(Filter): def get_dependency_name(self): return self.ref_name - def filter_options(self, options: Sequence[ParameterOption], trans, other_values): + def filter_options(self, options: Sequence[ParameterOption], trans: ProvidesHistoryContext, other_values): ref = other_values.get(self.ref_name, None) if ref is None: ref = [] @@ -348,7 +348,7 @@ class UniqueValueFilter(Filter): def get_dependency_name(self): return self.dynamic_option.dataset_ref_name - def filter_options(self, options: Sequence[ParameterOption], trans, other_values): + def filter_options(self, options: Sequence[ParameterOption], trans: ProvidesHistoryContext, other_values): rval = [] seen = set() for fields in options: @@ -377,7 +377,7 @@ class MultipleSplitterFilter(Filter): assert columns is not None, "Required 'column' attribute missing from filter" self.columns = [d_option.column_spec_to_index(column) for column in columns.split(",")] - def filter_options(self, options: Sequence[ParameterOption], trans, other_values): + def filter_options(self, options: Sequence[ParameterOption], trans: ProvidesHistoryContext, other_values): rval = [] for fields in options: for column in self.columns: @@ -412,7 +412,7 @@ class AttributeValueSplitterFilter(Filter): assert columns is not None, "Required 'column' attribute missing from filter" self.columns = [d_option.column_spec_to_index(column) for column in columns.split(",")] - def filter_options(self, options, trans, other_values): + def filter_options(self, options, trans: ProvidesHistoryContext, other_values): attr_names = set() rval = [] for fields in options: @@ -451,7 +451,7 @@ class AdditionalValueFilter(Filter): if self.index is not None: self.index = int(self.index) - def filter_options(self, options, trans, other_values): + def filter_options(self, options, trans: ProvidesHistoryContext, other_values): rval = list(options) add_value = [] for _ in range(self.dynamic_option.largest_index + 1): @@ -499,7 +499,7 @@ class RemoveValueFilter(Filter): self.multiple = string_as_bool(elem.get("multiple", "False")) self.separator = elem.get("separator", ",") - def filter_options(self, options, trans, other_values): + def filter_options(self, options, trans: ProvidesHistoryContext, other_values): from galaxy.tools.wrappers import DatasetFilenameWrapper if trans is not None and trans.workflow_building_mode: @@ -551,7 +551,7 @@ class SortByColumnFilter(Filter): self.column = d_option.column_spec_to_index(column) self.reverse = string_as_bool(elem.get("reverse_sort_order", "False")) - def filter_options(self, options, trans, other_values): + def filter_options(self, options, trans: ProvidesHistoryContext, other_values): return sorted(options, key=lambda x: x[self.column], reverse=self.reverse) @@ -586,7 +586,7 @@ class DataTableFilter(Filter): assert self.data_table_column is not None, "Required 'data_table_column' attribute missing from filter" self.keep = string_as_bool(elem.get("keep", "True")) - def filter_options(self, options, trans, other_values): + def filter_options(self, options, trans: ProvidesHistoryContext, other_values): # get column from data table, by index or column name entries = None try: @@ -786,7 +786,7 @@ class DynamicOptions: rval.append(depend) return rval - def get_fields(self, trans, other_values): + def get_fields(self, trans: ProvidesHistoryContext, other_values): if self.dataset_ref_name: try: datasets = _get_ref_data(other_values, self.dataset_ref_name) @@ -914,7 +914,7 @@ class DynamicOptions: assert len(entries) == 1, "Cannot pass tool data bundle with more than 1 data entry per table" return next(iter(entries.values())) - def get_fields_by_value(self, value, trans, other_values): + def get_fields_by_value(self, value, trans: ProvidesHistoryContext, other_values): """ Return a list of fields with column 'value' matching provided value. """ @@ -925,7 +925,7 @@ class DynamicOptions: rval.append(fields) return rval - def get_field_by_name_for_value(self, field_name, value, trans, other_values): + def get_field_by_name_for_value(self, field_name, value, trans: ProvidesHistoryContext, other_values): """ Get contents of field by name for specified value. """ @@ -942,7 +942,7 @@ class DynamicOptions: rval.append(fields[field_index]) return rval - def get_options(self, trans, other_values) -> Sequence[ParameterOption]: + def get_options(self, trans: ProvidesHistoryContext, other_values) -> Sequence[ParameterOption]: rval: list[ParameterOption] = [] diff --git a/lib/galaxy/tools/parameters/grouping.py b/lib/galaxy/tools/parameters/grouping.py index fe9147ecb47..b3a74d3446f 100644 --- a/lib/galaxy/tools/parameters/grouping.py +++ b/lib/galaxy/tools/parameters/grouping.py @@ -34,6 +34,7 @@ from galaxy.util.dictifiable import UsesDictVisibleKeys from galaxy.util.expressions import ExpressionContext if TYPE_CHECKING: + from galaxy.managers.context import ProvidesHistoryContext from galaxy.tools import Tool from galaxy.tools.parameters import ToolInputsT from galaxy.tools.parameters.basic import ToolParameter @@ -84,13 +85,13 @@ class Group(UsesDictVisibleKeys): """ return value - def get_initial_value(self, trans, context): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", context): """ Return the initial state/value for this group """ raise TypeError("Not implemented") - def to_dict(self, trans): + def to_dict(self, trans: "ProvidesHistoryContext"): group_dict = self._dictify_view_keys() return group_dict @@ -167,7 +168,7 @@ class Repeat(Group): raise return rval - def get_initial_value(self, trans, context): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", context): if self.inputs is None: raise Exception("Must set 'inputs' attribute to use.") rval = [] @@ -179,7 +180,7 @@ class Repeat(Group): rval.append(rval_dict) return rval - def to_dict(self, trans): + def to_dict(self, trans: "ProvidesHistoryContext"): if self.inputs is None: raise Exception("Must set 'inputs' attribute to use.") repeat_dict = super().to_dict(trans) @@ -235,7 +236,7 @@ class Section(Group): raise return rval - def get_initial_value(self, trans, context): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", context): if self.inputs is None: raise Exception("Must set 'inputs' attribute to use.") rval: dict[str, Any] = {} @@ -244,7 +245,7 @@ class Section(Group): rval[child_input.name] = child_input.get_initial_value(trans, child_context) return rval - def to_dict(self, trans): + def to_dict(self, trans: "ProvidesHistoryContext"): if self.inputs is None: raise Exception("Must set 'inputs' attribute to use.") section_dict = super().to_dict(trans) @@ -328,7 +329,7 @@ class UploadDataset(Group): dbkey = parent_context.get("dbkey", dbkey) return dbkey - def get_datatype_ext(self, trans, context, parent_context=None): + def get_datatype_ext(self, trans: "ProvidesHistoryContext", context, parent_context=None): ext = self.get_file_type(context, parent_context=parent_context) if ext in self.file_type_to_ext: ext = self.file_type_to_ext[ @@ -336,7 +337,7 @@ class UploadDataset(Group): ] # when using autodetect, we will use composite info from 'text', i.e. only the main file return ext - def get_datatype(self, trans, context, parent_context=None): + def get_datatype(self, trans: "ProvidesHistoryContext", context, parent_context=None): ext = self.get_datatype_ext(trans, context, parent_context=parent_context) return trans.app.datatypes_registry.get_datatype_by_extension(ext) @@ -347,7 +348,7 @@ class UploadDataset(Group): def group_title(self, context): return f"{self.title} ({context.get(self.file_type_name, self.default_file_type)})" - def title_by_index(self, trans, index, context): + def title_by_index(self, trans: "ProvidesHistoryContext", index, context): d_type = self.get_datatype(trans, context) for i, (composite_name, composite_file) in enumerate(d_type.writable_files.items()): if i == index: @@ -398,7 +399,7 @@ class UploadDataset(Group): raise return rval - def get_file_count(self, trans, context): + def get_file_count(self, trans: "ProvidesHistoryContext", context): file_count = context.get("file_count", "auto") if file_count == "auto": d_type = self.get_datatype(trans, context) @@ -406,9 +407,10 @@ class UploadDataset(Group): else: return int(file_count) - def get_initial_value(self, trans, context): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", context): if self.inputs is None: raise Exception("Must set 'inputs' attribute to use.") + assert trans is not None file_count = self.get_file_count(trans, context) rval = [] for i in range(file_count): @@ -419,7 +421,7 @@ class UploadDataset(Group): rval.append(rval_dict) return rval - def get_uploaded_datasets(self, trans, context, override_name=None, override_info=None): + def get_uploaded_datasets(self, trans: "ProvidesHistoryContext", context, override_name=None, override_info=None): def get_data_file_filename(data_file, override_name=None, override_info=None, purge=True): dataset_name = override_name @@ -510,6 +512,7 @@ class UploadDataset(Group): warnings.append("All FTP uploaded file selections were ignored.") elif ftp_files is not None and trans.user is not None: # look for files uploaded via FTP user_ftp_dir = trans.user_ftp_dir + assert user_ftp_dir is not None assert not os.path.islink(user_ftp_dir), "User FTP directory cannot be a symbolic link" for dirpath, _dirnames, filenames in os.walk(user_ftp_dir): for filename in filenames: @@ -598,16 +601,14 @@ class UploadDataset(Group): # TODO: warning to the user (could happen if session has become invalid) else: user_ftp_dir = trans.user_ftp_dir + assert user_ftp_dir is not None assert not os.path.islink(user_ftp_dir), "User FTP directory cannot be a symbolic link" for dirpath, _dirnames, filenames in os.walk(user_ftp_dir): for filename in filenames: path = relpath(os.path.join(dirpath, filename), user_ftp_dir) if not os.path.islink(os.path.join(dirpath, filename)): # Normalize filesystem paths - if isinstance(path, str): - valid_files.append(unicodedata.normalize("NFC", path)) - else: - valid_files.append(path) + valid_files.append(unicodedata.normalize("NFC", path)) else: ftp_files = [] @@ -616,6 +617,7 @@ class UploadDataset(Group): log.warning(f"User passed an invalid file path in ftp_files: {ftp_file}") continue # TODO: warning to the user (could happen if file is already imported) + assert user_ftp_dir is not None ftp_data_file = { "local_filename": os.path.abspath(os.path.join(user_ftp_dir, ftp_file)), "filename": os.path.basename(ftp_file), @@ -804,7 +806,7 @@ class Conditional(Group): raise return rval - def get_initial_value(self, trans, context): + def get_initial_value(self, trans: "ProvidesHistoryContext | None", context): if self.test_param is None: raise Exception("Must set 'test_param' attribute to use.") # State for a conditional is a plain dictionary. @@ -823,7 +825,7 @@ class Conditional(Group): rval[child_input.name] = child_input.get_initial_value(trans, child_context) return rval - def to_dict(self, trans): + def to_dict(self, trans: "ProvidesHistoryContext"): if self.test_param is None: raise Exception("Must set 'test_param' attribute to use.") cond_dict = super().to_dict(trans) @@ -843,7 +845,7 @@ class ConditionalWhen(UsesDictVisibleKeys): self.value = None self.inputs = None - def to_dict(self, trans): + def to_dict(self, trans: "ProvidesHistoryContext"): if self.inputs is None: raise Exception("Must set 'inputs' attribute to use.") when_dict = self._dictify_view_keys() diff --git a/lib/galaxy/tools/recommendations.py b/lib/galaxy/tools/recommendations.py index a4cb3a5648e..43ba09e161c 100644 --- a/lib/galaxy/tools/recommendations.py +++ b/lib/galaxy/tools/recommendations.py @@ -3,11 +3,19 @@ import json import logging import os +from collections.abc import Iterable +from typing import ( + Any, +) import h5py import numpy as np import yaml +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesHistoryContext, +) from galaxy.tools.parameters import populate_state from galaxy.tools.parameters.workflow_utils import workflow_building_modes from galaxy.util import ( @@ -115,7 +123,7 @@ class ToolRecommendations: outputs = Dense(vocab_size, activation="sigmoid")(x) return Model(inputs=inputs, outputs=[outputs, weights]) - def get_predictions(self, trans, tool_sequence, remote_model_url): + def get_predictions(self, trans: ProvidesHistoryContext, tool_sequence, remote_model_url): """ Compute tool predictions """ @@ -126,7 +134,7 @@ class ToolRecommendations: recommended_tools = self.__compute_tool_prediction(trans, tool_sequence) return tool_sequence, recommended_tools - def __set_model(self, trans, remote_model_url): + def __set_model(self, trans: ProvidesAppContext, remote_model_url): """ Create model and associated dictionaries for recommendations """ @@ -185,7 +193,7 @@ class ToolRecommendations: model_file.write(model_binary.content) return local_dir - def __get_tool_extensions(self, trans, tool_id): + def __get_tool_extensions(self, trans: ProvidesHistoryContext, tool_id): """ Get the input and output extensions of a tool """ @@ -194,7 +202,7 @@ class ToolRecommendations: trans.workflow_building_mode = workflow_building_modes.ENABLED module = module_factory.from_dict(trans, payload) if "tool_state" not in payload: - module_state = {} + module_state: dict[str, Any] = {} populate_state(trans, module.get_inputs(), inputs, module_state, check=False) module.recover_state(module_state) inputs = module.get_all_inputs(connectable_only=True) @@ -207,7 +215,9 @@ class ToolRecommendations: output_extensions.extend(o_ext["extensions"]) return input_extensions, output_extensions - def __filter_tool_predictions(self, trans, prediction_data, tool_ids, tool_scores, last_tool_name): + def __filter_tool_predictions( + self, trans: ProvidesHistoryContext, prediction_data, tool_ids, tool_scores, last_tool_name + ): """ Filter tool predictions based on datatype compatibility and tool connections. Add admin preferences to recommendations. @@ -216,7 +226,7 @@ class ToolRecommendations: # get the list of datatype extensions of the last tool of the tool sequence _, last_output_extensions = self.__get_tool_extensions(trans, self.all_tools[last_tool_name][0]) prediction_data["o_extensions"] = list(set(last_output_extensions)) - t_ids_scores = zip(tool_ids, tool_scores) + t_ids_scores: Iterable[tuple[Any, Any]] = zip(tool_ids, tool_scores) # form the payload of the predicted tools to be shown for child, score in t_ids_scores: c_dict = {} @@ -327,14 +337,14 @@ class ToolRecommendations: sorted_c_t, sorted_c_v = self.__get_predicted_tools(last_base_tools, pred_tool_names, last_tool_name, topk) return sorted_c_t, sorted_c_v - def __compute_tool_prediction(self, trans, tool_sequence): + def __compute_tool_prediction(self, trans: ProvidesHistoryContext, tool_sequence): """ Compute the predicted tools for a tool sequences Return a payload with the tool sequences and recommended tools Return an empty payload with just the tool sequence if anything goes wrong within the try block """ topk = trans.app.config.topk_recommendations - prediction_data = {} + prediction_data: dict[str, Any] = {} tool_sequence = tool_sequence.split(",")[::-1] prediction_data["name"] = ",".join(tool_sequence) prediction_data["children"] = [] @@ -351,15 +361,14 @@ class ToolRecommendations: except Exception: log.exception(f"Failed to find tool {tool_name} in model") return prediction_data - sample = np.reshape(sample, (1, self.max_seq_len)) + model_input = np.reshape(sample, (1, self.max_seq_len)) # boost the predicted scores using tools' usage weight_values = list(self.tool_weights_sorted.values()) # predict next tools for a test path try: import tensorflow as tf - sample = tf.convert_to_tensor(sample, dtype=tf.int64) - prediction, _ = self.loaded_model(sample, training=False) + prediction, _ = self.loaded_model(tf.convert_to_tensor(model_input, dtype=tf.int64), training=False) except Exception as e: log.exception(e) return prediction_data diff --git a/lib/galaxy/tools/wrappers.py b/lib/galaxy/tools/wrappers.py index 62a6ffecec9..ea9a7f896a3 100644 --- a/lib/galaxy/tools/wrappers.py +++ b/lib/galaxy/tools/wrappers.py @@ -49,6 +49,7 @@ from galaxy.util import ( ) if TYPE_CHECKING: + from galaxy.datatypes.data import Data from galaxy.datatypes.registry import Registry from galaxy.job_execution.compute_environment import ComputeEnvironment from galaxy.model.metadata import MetadataCollection @@ -359,7 +360,7 @@ class DatasetFilenameWrapper(ToolParameterValueWrapper): compute_environment: Optional["ComputeEnvironment"] = None, identifier: str | None = None, io_type: str = "input", - formats: list[str] | None = None, + formats: Sequence[Union[str, "Data"]] | None = None, tool_evaluator: Optional["ToolEvaluator"] = None, ) -> None: dataset_instance: DatasetInstance | None = None diff --git a/lib/galaxy/util/tool_shed/common_util.py b/lib/galaxy/util/tool_shed/common_util.py index fddc8e0dffa..e400f871c91 100644 --- a/lib/galaxy/util/tool_shed/common_util.py +++ b/lib/galaxy/util/tool_shed/common_util.py @@ -12,6 +12,7 @@ from galaxy import util from galaxy.util.tool_shed import encoding_util if TYPE_CHECKING: + from galaxy.webapps.base.webapp import GalaxyWebTransaction from .tool_shed_registry import Registry as ToolShedRegistry log = logging.getLogger(__name__) @@ -149,7 +150,7 @@ def get_tool_shed_repository_url(app: HasToolShedRegistry, tool_shed: str, owner return tool_shed_url -def handle_galaxy_url(trans, **kwd): +def handle_galaxy_url(trans: "GalaxyWebTransaction", **kwd): galaxy_url = kwd.get("galaxy_url", None) if galaxy_url: trans.set_cookie(galaxy_url, name="toolshedgalaxyurl") diff --git a/lib/galaxy/visualization/data_providers/registry.py b/lib/galaxy/visualization/data_providers/registry.py index 5d65847c973..778801e31f8 100644 --- a/lib/galaxy/visualization/data_providers/registry.py +++ b/lib/galaxy/visualization/data_providers/registry.py @@ -21,6 +21,7 @@ from galaxy.datatypes.tabular import ( ) from galaxy.datatypes.xml import Phyloxml from galaxy.exceptions import RequestParameterInvalidException +from galaxy.managers.context import ProvidesAppContext from galaxy.model import NoConverterException from galaxy.visualization.data_providers import genome from galaxy.visualization.data_providers.basic import ( @@ -61,7 +62,7 @@ class DataProviderRegistry: "column_with_stats": ColumnDataProvider, } - def get_data_provider(self, trans, name=None, source="data", raw=False, original_dataset=None): + def get_data_provider(self, trans: ProvidesAppContext, name=None, source="data", raw=False, original_dataset=None): """ Returns data provider matching parameter values. For standalone data sources, source parameter is ignored. diff --git a/lib/galaxy/visualization/genomes.py b/lib/galaxy/visualization/genomes.py index fe7f9c210a1..296b87c3401 100644 --- a/lib/galaxy/visualization/genomes.py +++ b/lib/galaxy/visualization/genomes.py @@ -10,6 +10,10 @@ from galaxy.exceptions import ( ObjectNotFound, ReferenceDataError, ) +from galaxy.managers.context import ( + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.model import ( HistoryDatasetAssociation, User, @@ -286,7 +290,7 @@ class Genomes: return dbkeys - def chroms(self, trans, dbkey=None, num=None, chrom=None, low=None): + def chroms(self, trans: ProvidesHistoryContext, dbkey=None, num=None, chrom=None, low=None): """ Returns a naturally sorted list of chroms/contigs for a given dbkey. Use either chrom or low to specify the starting chrom in the return list. @@ -364,7 +368,7 @@ class Genomes: return False - def reference(self, trans, dbkey, chrom, low, high): + def reference(self, trans: ProvidesUserContext, dbkey, chrom, low, high): """ Return reference data for a build. """ diff --git a/lib/galaxy/visualization/plugins/datasource_testing.py b/lib/galaxy/visualization/plugins/datasource_testing.py index cb922b4cc30..c1739668347 100644 --- a/lib/galaxy/visualization/plugins/datasource_testing.py +++ b/lib/galaxy/visualization/plugins/datasource_testing.py @@ -1,5 +1,7 @@ import logging +from galaxy.managers.context import ProvidesAppContext + log = logging.getLogger(__name__) @@ -29,7 +31,7 @@ def _deferred_source_uri(target_object) -> str | None: return None -def is_object_applicable(trans, target_object, data_source_tests): +def is_object_applicable(trans: ProvidesAppContext, target_object, data_source_tests): """ Run a visualization's data_source tests to find out if it can be applied to the target_object. diff --git a/lib/galaxy/visualization/plugins/registry.py b/lib/galaxy/visualization/plugins/registry.py index 43532d09d57..5a555244cf0 100644 --- a/lib/galaxy/visualization/plugins/registry.py +++ b/lib/galaxy/visualization/plugins/registry.py @@ -20,6 +20,7 @@ from galaxy.visualization.plugins.datasource_testing import is_object_applicable from galaxy.visualization.plugins.plugin import VisualizationPlugin if TYPE_CHECKING: + from galaxy.managers.context import ProvidesAppContext from galaxy.structured_app import StructuredApp log = logging.getLogger(__name__) @@ -150,7 +151,7 @@ class VisualizationsRegistry: return self.plugins[key] # -- building links to visualizations from objects -- - def get_visualizations(self, trans, target_object=None, embeddable=None): + def get_visualizations(self, trans: "ProvidesAppContext", target_object=None, embeddable=None): """ Get the names of visualizations usable on the `target_object` and the urls to call in order to render the visualizations. @@ -166,7 +167,7 @@ class VisualizationsRegistry: result.append(vis_plugin.to_dict()) return sorted(result, key=lambda k: k.get("html")) - def get_visualization(self, trans, visualization_name, target_object): + def get_visualization(self, trans: "ProvidesAppContext", visualization_name, target_object): """ Return data to build a url to the visualization with the given `visualization_name` if it's applicable to `target_object` or diff --git a/lib/galaxy/visualization/plugins/resource_parser.py b/lib/galaxy/visualization/plugins/resource_parser.py index 9c58075e8d2..2e0edc3e561 100644 --- a/lib/galaxy/visualization/plugins/resource_parser.py +++ b/lib/galaxy/visualization/plugins/resource_parser.py @@ -7,6 +7,7 @@ import json import logging import weakref from collections.abc import Callable +from typing import TYPE_CHECKING import galaxy.exceptions import galaxy.util @@ -14,6 +15,7 @@ from galaxy.managers import ( hdas as hda_manager, visualizations as visualization_manager, ) +from galaxy.managers.context import ProvidesUserContext from galaxy.model import ( HistoryDatasetAssociation, LibraryDatasetDatasetAssociation, @@ -21,6 +23,9 @@ from galaxy.model import ( ) from galaxy.util import bunch +if TYPE_CHECKING: + from galaxy.webapps.base.webapp import GalaxyWebTransaction + log = logging.getLogger(__name__) @@ -59,7 +64,9 @@ class ResourceParser: hda=app[hda_manager.HDAManager], ) - def parse_parameter_dictionary(self, trans, param_config_dict, query_params, param_modifiers=None): + def parse_parameter_dictionary( + self, trans: "GalaxyWebTransaction", param_config_dict, query_params, param_modifiers=None + ): """ Parse all expected params from the query dictionary `query_params`. @@ -111,7 +118,7 @@ class ResourceParser: return resources - def parse_config(self, trans, param_config_dict, query_params): + def parse_config(self, trans: ProvidesUserContext, param_config_dict, query_params): """ Return `query_params` dict parsing only JSON serializable params. Complex params such as models, etc. are left as the original query value. @@ -149,7 +156,7 @@ class ResourceParser: # TODO: I would LOVE to rip modifiers out completely def parse_parameter_modifiers( - self, trans, param_modifiers, query_params + self, trans: ProvidesUserContext, param_modifiers, query_params ) -> dict[str, dict[str, ParameterType | None]]: """ Parse and return parameters that are meant to modify other parameters, @@ -177,7 +184,7 @@ class ResourceParser: return parsed_modifiers - def parse_parameter_default(self, trans, param_config) -> ParameterType | None: + def parse_parameter_default(self, trans: ProvidesUserContext, param_config) -> ParameterType | None: """ Parse any default values for the given param, defaulting the default to `None`. @@ -192,7 +199,9 @@ class ResourceParser: # (and adding this code to the xml parser) return self.parse_parameter(trans, param_config, default) - def parse_parameter(self, trans, expected_param_data, query_param, recurse=True, param_modifiers=None): + def parse_parameter( + self, trans: ProvidesUserContext, expected_param_data, query_param, recurse=True, param_modifiers=None + ): """ Use data in `expected_param_data` to parse `query_param` from a string into a resource usable directly by a template. diff --git a/lib/galaxy/web/framework/base.py b/lib/galaxy/web/framework/base.py index 454aee79cc2..05430d38c52 100644 --- a/lib/galaxy/web/framework/base.py +++ b/lib/galaxy/web/framework/base.py @@ -16,7 +16,10 @@ from http.cookies import ( SimpleCookie, ) from importlib import import_module -from typing import NoReturn +from typing import ( + NoReturn, + TYPE_CHECKING, +) from urllib.parse import urljoin import routes @@ -31,6 +34,9 @@ from paste.response import HeaderDict from galaxy.util import smart_str from galaxy.util.resources import resource_string +if TYPE_CHECKING: + from galaxy.webapps.base.webapp import GalaxyWebTransaction + log = logging.getLogger(__name__) #: time of the most recent server startup @@ -281,7 +287,7 @@ class WebApplication: body_renderer = body_renderer or self._render_body return body_renderer(trans, body, environ, start_response) - def _render_body(self, trans, body, environ, start_response): + def _render_body(self, trans: "GalaxyWebTransaction", body, environ, start_response): # Now figure out what we got back and try to get it to the browser in # a smart way if callable(body): @@ -299,7 +305,7 @@ class WebApplication: start_response(trans.response.wsgi_status(), trans.response.wsgi_headeritems()) return self.make_body_iterable(trans, body) - def make_body_iterable(self, trans, body): + def make_body_iterable(self, trans: "DefaultWebTransaction", body): if isinstance(body, (types.GeneratorType, list, tuple)): # Recursively stream the iterable return flatten(body) @@ -310,7 +316,7 @@ class WebApplication: # Worst case scenario return [smart_str(body)] - def handle_controller_exception(self, e, trans, method, kwargs): + def handle_controller_exception(self, e, trans: "GalaxyWebTransaction", method, kwargs): """ Allow handling of exceptions raised in controller methods. """ @@ -366,6 +372,10 @@ class DefaultWebTransaction: self.environ = environ self.request = Request(environ) self.response = Response() + # Set by WebApplication.handle_request() once the route is resolved. + self.request_id: str | None = None + self.controller: str | None = None + self.action: str | None = None @lazy_property def session(self): @@ -547,7 +557,7 @@ class Response: CHUNK_SIZE = 2**16 -def send_file(start_response, trans, body): +def send_file(start_response, trans: "GalaxyWebTransaction", body): # If configured use X-Accel-Redirect header for nginx base = trans.app.config.nginx_x_accel_redirect_base apache_xsendfile = trans.app.config.apache_xsendfile diff --git a/lib/galaxy/web/framework/decorators.py b/lib/galaxy/web/framework/decorators.py index b76fd717d4d..945da7a55cc 100644 --- a/lib/galaxy/web/framework/decorators.py +++ b/lib/galaxy/web/framework/decorators.py @@ -3,6 +3,7 @@ from functools import wraps from inspect import getfullargspec from json import loads from traceback import format_exc +from typing import TYPE_CHECKING import paste.httpexceptions from pydantic import ( @@ -26,6 +27,9 @@ from galaxy.util import ( from galaxy.util.json import safe_dumps from galaxy.web.framework import url_for +if TYPE_CHECKING: + from galaxy.webapps.base.webapp import GalaxyWebTransaction + log = logging.getLogger(__name__) JSON_CONTENT_TYPE = "application/json; charset=UTF-8" @@ -59,7 +63,7 @@ def json(func, pretty=False): """ @wraps(func) - def call_and_format(self, trans, *args, **kwargs): + def call_and_format(self, trans: "GalaxyWebTransaction", *args, **kwargs): # pull out any callback argument to the api endpoint and set the content type to json or javascript jsonp_callback = kwargs.pop(JSONP_CALLBACK_KEY, None) if jsonp_callback: @@ -84,7 +88,7 @@ def json_pretty(func): def require_login(verb="perform this action", use_panels=False): def argcatcher(func): @wraps(func) - def decorator(self, trans, *args, **kwargs): + def decorator(self, trans: "GalaxyWebTransaction", *args, **kwargs): if trans.get_user(): return func(self, trans, *args, **kwargs) else: @@ -105,7 +109,7 @@ def require_login(verb="perform this action", use_panels=False): def require_admin(func): @wraps(func) - def decorator(self, trans, *args, **kwargs): + def decorator(self, trans: "GalaxyWebTransaction", *args, **kwargs): if not trans.user_is_admin: msg = require_admin_message(trans.app.config, trans.get_user()) trans.response.status = 403 @@ -136,7 +140,7 @@ def do_not_cache(func): """ @wraps(func) - def set_nocache_headers(self, trans, *args, **kwargs): + def set_nocache_headers(self, trans: "GalaxyWebTransaction", *args, **kwargs): trans.response.headers["Cache-Control"] = ["no-cache", "no-store", "must-revalidate"] trans.response.headers["Pragma"] = "no-cache" trans.response.headers["Expires"] = "0" @@ -152,7 +156,7 @@ def legacy_expose_api(func, to_json=True, user_required=True): """ @wraps(func) - def decorator(self, trans, *args, **kwargs): + def decorator(self, trans: "GalaxyWebTransaction", *args, **kwargs): def error(environ, start_response): start_response(error_status, [("Content-type", "text/plain")]) return [smart_str(error_message)] @@ -211,7 +215,7 @@ def legacy_expose_api(func, to_json=True, user_required=True): return expose(_save_orig_fn(decorator, func)) -def __extract_payload_from_request(trans, func, kwargs): +def __extract_payload_from_request(trans: "GalaxyWebTransaction", func, kwargs): content_type = trans.request.headers.get("content-type", "") if content_type.startswith("application/x-www-form-urlencoded") or content_type.startswith("multipart/form-data"): # If the content type is a standard type such as multipart/form-data, the wsgi framework parses the request body @@ -276,7 +280,7 @@ def expose_api(func, to_json=True, user_required=True, user_or_session_required= """ @wraps(func) - def decorator(self, trans, *args, **kwargs): + def decorator(self, trans: "GalaxyWebTransaction", *args, **kwargs): # errors passed in from trans._authenticate_api if trans.error_message: return __api_error_response( @@ -385,14 +389,14 @@ def format_return_as_json(rval, jsonp_callback=None, pretty=False): return json -def __api_error_dict(trans, **kwds): +def __api_error_dict(trans: "GalaxyWebTransaction", **kwds): error_dict = api_error_to_dict(debug=trans.debug, **kwds) exception = kwds.get("exception", None) # If we are given an status code directly - use it - otherwise check # the exception for a status_code attribute. if "status_code" in kwds: - status_code = int(kwds.get("status_code")) - elif hasattr(exception, "status_code"): + status_code = int(kwds["status_code"]) + elif exception is not None and hasattr(exception, "status_code"): status_code = int(exception.status_code) else: status_code = 500 @@ -405,7 +409,7 @@ def __api_error_dict(trans, **kwds): return error_dict -def __api_error_response(trans, **kwds): +def __api_error_response(trans: "GalaxyWebTransaction", **kwds): error_dict = __api_error_dict(trans, **kwds) return safe_dumps(error_dict) diff --git a/lib/galaxy/web/framework/helpers/grids.py b/lib/galaxy/web/framework/helpers/grids.py index eb1c6e07bd2..f7ddc1faf36 100644 --- a/lib/galaxy/web/framework/helpers/grids.py +++ b/lib/galaxy/web/framework/helpers/grids.py @@ -1,4 +1,5 @@ import logging +from typing import TYPE_CHECKING from markupsafe import escape @@ -7,6 +8,9 @@ from galaxy.util import ( unicodify, ) +if TYPE_CHECKING: + from galaxy.webapps.base.webapp import GalaxyWebTransaction + log = logging.getLogger(__name__) @@ -28,7 +32,7 @@ class GridColumn: self.format = format self.escape = escape - def get_value(self, trans, grid, item): + def get_value(self, trans: "GalaxyWebTransaction", grid, item): if self.method: value = getattr(grid, self.method)(trans, item) elif self.key and hasattr(item, self.key): @@ -42,7 +46,7 @@ class GridColumn: else: return value - def sort(self, trans, query, ascending, column_name=None): + def sort(self, trans: "GalaxyWebTransaction", query, ascending, column_name=None): """Sort query using this column.""" if column_name is None: column_name = self.key @@ -64,6 +68,8 @@ class GridData: model_class: type | None = None columns: list[GridColumn] = [] default_limit: int = 1000 + # Subclasses provide the default sort column key. + default_sort_key: str def __init__(self): # If a column does not have a model class, set the column's model class @@ -72,7 +78,11 @@ class GridData: if not column.model_class: column.model_class = self.model_class - def __call__(self, trans, **kwargs): + def apply_query_filter(self, query, **kwargs): + # Subclasses override this to restrict the grid's base query. + raise NotImplementedError + + def __call__(self, trans: "GalaxyWebTransaction", **kwargs): limit = kwargs.get("limit", self.default_limit) offset = kwargs.get("offset", 0) diff --git a/lib/galaxy/web/proxy/__init__.py b/lib/galaxy/web/proxy/__init__.py index eedcfb720c6..7f706171ada 100644 --- a/lib/galaxy/web/proxy/__init__.py +++ b/lib/galaxy/web/proxy/__init__.py @@ -3,6 +3,7 @@ import logging import os import time from collections import namedtuple +from typing import TYPE_CHECKING from galaxy.util import ( requests, @@ -17,6 +18,10 @@ from galaxy.util.lazy_process import ( ) from galaxy.web.framework import url_for +if TYPE_CHECKING: + from galaxy.config import GalaxyAppConfiguration + from galaxy.webapps.base.webapp import GalaxyWebTransaction + log = logging.getLogger(__name__) @@ -31,23 +36,20 @@ class ProxyManager: "port", ) - def __init__(self, config): - for option in [ - "manage_dynamic_proxy", - "dynamic_proxy_bind_port", - "dynamic_proxy_bind_ip", - "dynamic_proxy_debug", - "dynamic_proxy_external_proxy", - "dynamic_proxy_prefix", - "proxy_session_map", - "dynamic_proxy", - "cookie_path", - "dynamic_proxy_golang_noaccess", - "dynamic_proxy_golang_clean_interval", - "dynamic_proxy_golang_docker_address", - "dynamic_proxy_golang_api_key", - ]: - setattr(self, option, getattr(config, option)) + def __init__(self, config: "GalaxyAppConfiguration"): + self.manage_dynamic_proxy = config.manage_dynamic_proxy + self.dynamic_proxy_bind_port = config.dynamic_proxy_bind_port + self.dynamic_proxy_bind_ip = config.dynamic_proxy_bind_ip + self.dynamic_proxy_debug = config.dynamic_proxy_debug + self.dynamic_proxy_external_proxy = config.dynamic_proxy_external_proxy + self.dynamic_proxy_prefix = config.dynamic_proxy_prefix + self.proxy_session_map = config.proxy_session_map + self.dynamic_proxy = config.dynamic_proxy + self.cookie_path = config.cookie_path + self.dynamic_proxy_golang_noaccess = config.dynamic_proxy_golang_noaccess + self.dynamic_proxy_golang_clean_interval = config.dynamic_proxy_golang_clean_interval + self.dynamic_proxy_golang_docker_address = config.dynamic_proxy_golang_docker_address + self.dynamic_proxy_golang_api_key = config.dynamic_proxy_golang_api_key if self.manage_dynamic_proxy: self.lazy_process = self.__setup_lazy_process(config) @@ -64,7 +66,7 @@ class ProxyManager: def setup_proxy( self, - trans, + trans: "GalaxyWebTransaction", host=DEFAULT_PROXY_TO_HOST, port=None, proxy_prefix="", @@ -105,14 +107,14 @@ class ProxyManager: "proxied_host": proxy_requests.host, } - def update_proxy(self, trans, **kwargs): + def update_proxy(self, trans: "GalaxyWebTransaction", **kwargs): authentication = AuthenticationToken(trans) for k in kwargs.keys(): if k not in self.valid_update_keys: raise Exception(f"Invalid proxy request update key: {k}") return self.proxy_ipc.update_requests(authentication, **kwargs) - def query_proxy(self, trans): + def query_proxy(self, trans: "GalaxyWebTransaction"): authentication = AuthenticationToken(trans) return self.proxy_ipc.fetch_requests(authentication) @@ -182,7 +184,7 @@ class GolangProxyLauncher: class AuthenticationToken: - def __init__(self, trans): + def __init__(self, trans: "GalaxyWebTransaction"): self.cookie_name = SECURE_COOKIE self.cookie_value = trans.get_cookie(self.cookie_name) diff --git a/lib/galaxy/webapps/base/controller.py b/lib/galaxy/webapps/base/controller.py index e91471acb16..5304400ea85 100644 --- a/lib/galaxy/webapps/base/controller.py +++ b/lib/galaxy/webapps/base/controller.py @@ -6,6 +6,7 @@ import logging from collections.abc import Callable from typing import ( Any, + Protocol, TYPE_CHECKING, ) @@ -27,6 +28,10 @@ from galaxy.managers import ( users, workflows, ) +from galaxy.managers.context import ( + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.managers.forms import ( get_filtered_form_definitions_current, get_form_definitions, @@ -62,6 +67,7 @@ from galaxy.workflow.modules import WorkflowModuleInjector if TYPE_CHECKING: from galaxy.structured_app import StructuredApp + from galaxy.webapps.base.webapp import GalaxyWebTransaction log = logging.getLogger(__name__) @@ -88,7 +94,9 @@ class BaseController: """Returns the class object that a string denotes. Without this method, we'd have to do eval().""" return managers_base.get_class(class_name) - def get_object(self, trans, id, class_name, check_ownership=False, check_accessible=False, deleted=None): + def get_object( + self, trans: ProvidesUserContext, id, class_name, check_ownership=False, check_accessible=False, deleted=None + ): """ Convenience method to get a model object with the specified checks. """ @@ -103,20 +111,20 @@ class BaseController: # # meant to be overridden in SharableSecurityMixin # return item - def get_user(self, trans, id, check_ownership=False, check_accessible=False, deleted=None): + def get_user(self, trans: ProvidesUserContext, id, check_ownership=False, check_accessible=False, deleted=None): return self.get_object(trans, id, "User", check_ownership=False, check_accessible=False, deleted=deleted) - def get_group(self, trans, id, check_ownership=False, check_accessible=False, deleted=None): + def get_group(self, trans: ProvidesUserContext, id, check_ownership=False, check_accessible=False, deleted=None): return self.get_object(trans, id, "Group", check_ownership=False, check_accessible=False, deleted=deleted) - def get_role(self, trans, id, check_ownership=False, check_accessible=False, deleted=None): + def get_role(self, trans: ProvidesUserContext, id, check_ownership=False, check_accessible=False, deleted=None): return self.get_object(trans, id, "Role", check_ownership=False, check_accessible=False, deleted=deleted) # ---- parsing query params def decode_id(self, id): return managers_base.decode_id(self.app, id) - def encode_all_ids(self, trans, rval, recursive=False): + def encode_all_ids(self, trans: ProvidesUserContext, rval, recursive=False): """ Encodes all integer values in the dict rval whose keys are 'id' or end with '_id' @@ -176,7 +184,9 @@ Root = BaseController class BaseUIController(BaseController): - def get_object(self, trans, id, class_name, check_ownership=False, check_accessible=False, deleted=None): + def get_object( + self, trans: ProvidesUserContext, id, class_name, check_ownership=False, check_accessible=False, deleted=None + ): try: return BaseController.get_object( self, @@ -193,13 +203,15 @@ class BaseUIController(BaseController): log.exception("Exception in get_object check for %s %s:", class_name, str(id)) raise Exception(f"Server error retrieving {class_name} id ( {str(id)} ).") - def message_exception(self, trans, message, sanitize=True): + def message_exception(self, trans: "GalaxyWebTransaction", message, sanitize=True): trans.response.status = 400 return {"err_msg": util.sanitize_text(message) if sanitize else message} class BaseAPIController(BaseController): - def get_object(self, trans, id, class_name, check_ownership=False, check_accessible=False, deleted=None): + def get_object( + self, trans: ProvidesUserContext, id, class_name, check_ownership=False, check_accessible=False, deleted=None + ): try: return BaseController.get_object( self, @@ -217,7 +229,7 @@ class BaseAPIController(BaseController): log.exception("Exception in get_object check for %s %s.", class_name, str(id)) raise HTTPInternalServerError(comment=util.unicodify(e)) - def not_implemented(self, trans, **kwd): + def not_implemented(self, trans: ProvidesUserContext, **kwd): raise HTTPNotImplemented() def _parse_serialization_params(self, kwd, default_view): @@ -253,7 +265,7 @@ class Datatype: class SharableItemSecurityMixin: """Mixin for handling security for sharable items.""" - def security_check(self, trans, item, check_ownership=False, check_accessible=False): + def security_check(self, trans: ProvidesUserContext, item, check_ownership=False, check_accessible=False): """Security checks for an item: checks if (a) user owns item or (b) item is accessible to user.""" return managers_base.security_check( trans, item, check_ownership=check_ownership, check_accessible=check_accessible @@ -263,10 +275,12 @@ class SharableItemSecurityMixin: class UsesLibraryMixinItems(SharableItemSecurityMixin): get_object: Callable - def get_library_folder(self, trans, id: int, check_ownership=False, check_accessible=True): + def get_library_folder(self, trans: ProvidesUserContext, id: int, check_ownership=False, check_accessible=True): return self.get_object(trans, id, "LibraryFolder", check_ownership=False, check_accessible=check_accessible) - def get_library_dataset_dataset_association(self, trans, id, check_ownership=False, check_accessible=True): + def get_library_dataset_dataset_association( + self, trans: ProvidesUserContext, id, check_ownership=False, check_accessible=True + ): # Deprecated in lieu to galaxy.managers.lddas.LDDAManager.get() but not # reusing that exactly because of subtle differences in exception handling # logic (API controller override get_object to be slightly different). @@ -274,7 +288,7 @@ class UsesLibraryMixinItems(SharableItemSecurityMixin): trans, id, "LibraryDatasetDatasetAssociation", check_ownership=False, check_accessible=check_accessible ) - def get_library_dataset(self, trans, id, check_ownership=False, check_accessible=True): + def get_library_dataset(self, trans: ProvidesUserContext, id, check_ownership=False, check_accessible=True): return self.get_object(trans, id, "LibraryDataset", check_ownership=False, check_accessible=check_accessible) # TODO: it makes no sense that I can get roles from a user but not user.is_admin() @@ -283,14 +297,14 @@ class UsesLibraryMixinItems(SharableItemSecurityMixin): # return ( ( user.is_admin() ) # or ( trans.app.security_agent.can_add_library_item( user.all_roles(), item ) ) ) - def can_current_user_add_to_library_item(self, trans, item): + def can_current_user_add_to_library_item(self, trans: ProvidesUserContext, item): if not trans.user: return False return trans.user_is_admin or trans.app.security_agent.can_add_library_item( trans.get_current_user_roles(), item ) - def check_user_can_add_to_library_item(self, trans, item, check_accessible=True): + def check_user_can_add_to_library_item(self, trans: ProvidesUserContext, item, check_accessible=True): """ Raise exception if user cannot add to the specified library item (i.e. Folder). Can set check_accessible to False if folder was loaded with @@ -311,7 +325,9 @@ class UsesLibraryMixinItems(SharableItemSecurityMixin): # Slight misuse of ItemOwnershipException? raise exceptions.ItemOwnershipException("User cannot add to library item.") - def _copy_hdca_to_library_folder(self, trans, hda_manager, from_hdca_id: int, folder_id: int, ldda_message=""): + def _copy_hdca_to_library_folder( + self, trans: ProvidesHistoryContext, hda_manager, from_hdca_id: int, folder_id: int, ldda_message="" + ): """ Fetches the collection identified by `from_hcda_id` and dispatches individual collection elements to _copy_hda_to_library_folder @@ -337,7 +353,13 @@ class UsesLibraryMixinItems(SharableItemSecurityMixin): ] def _copy_hda_to_library_folder( - self, trans, hda_manager, from_hda_id: int, folder_id: int, ldda_message="", element_identifier=None + self, + trans: ProvidesHistoryContext, + hda_manager, + from_hda_id: int, + folder_id: int, + ldda_message="", + element_identifier=None, ): """ Copies hda ``from_hda_id`` to library folder ``folder_id``, optionally @@ -383,7 +405,7 @@ class UsesLibraryMixinItems(SharableItemSecurityMixin): return rval def copy_hda_to_library_folder( - self, trans, hda, library_folder, roles=None, ldda_message="", element_identifier=None + self, trans: ProvidesUserContext, hda, library_folder, roles=None, ldda_message="", element_identifier=None ): # PRECONDITION: permissions for this action on hda and library_folder have been checked roles = roles or [] @@ -407,7 +429,7 @@ class UsesLibraryMixinItems(SharableItemSecurityMixin): # then finally, re-applies hda -> ldda for missing actions in _apply_hda_permissions_to_ldda?? return ldda - def _apply_library_folder_permissions_to_ldda(self, trans, library_folder, ldda): + def _apply_library_folder_permissions_to_ldda(self, trans: ProvidesUserContext, library_folder, ldda): """ Copy actions/roles from library folder to an ldda (and its library_dataset). """ @@ -417,7 +439,7 @@ class UsesLibraryMixinItems(SharableItemSecurityMixin): security_agent.copy_library_permissions(trans, library_folder, ldda.library_dataset) return security_agent.get_permissions(ldda) - def _apply_hda_permissions_to_ldda(self, trans, hda, ldda): + def _apply_hda_permissions_to_ldda(self, trans: ProvidesUserContext, hda, ldda): """ Copy actions/roles from hda to ldda.library_dataset (and then ldda) if ldda doesn't already have roles for the given action. @@ -432,8 +454,11 @@ class UsesLibraryMixinItems(SharableItemSecurityMixin): # except that: if DATASET_MANAGE_PERMISSIONS exists in the hda.dataset permissions, # we need to instead apply those roles to the LIBRARY_MANAGE permission to the library dataset - dataset_manage_permissions_action = security_agent.get_action("DATASET_MANAGE_PERMISSIONS").action - library_manage_permissions_action = security_agent.get_action("LIBRARY_MANAGE").action + dataset_manage_permissions = security_agent.get_action("DATASET_MANAGE_PERMISSIONS") + library_manage_permissions = security_agent.get_action("LIBRARY_MANAGE") + assert dataset_manage_permissions is not None and library_manage_permissions is not None + dataset_manage_permissions_action = dataset_manage_permissions.action + library_manage_permissions_action = library_manage_permissions.action # TODO: test this and remove if in loop below # TODO: doesn't handle action.action # if dataset_manage_permissions_action in dataset_permissions_dict: @@ -454,11 +479,9 @@ class UsesLibraryMixinItems(SharableItemSecurityMixin): # NOTE: only apply an hda perm if it's NOT set in the library_dataset perms (don't overwrite) if action not in library_dataset_actions: for role in dataset_permissions_roles: - ldps = LibraryDatasetPermissions(action, library_dataset, role) - ldps = [ldps] if not isinstance(ldps, list) else ldps - for ldp in ldps: - trans.sa_session.add(ldp) - flush_needed = True + ldp = LibraryDatasetPermissions(action, library_dataset, role) + trans.sa_session.add(ldp) + flush_needed = True if flush_needed: trans.sa_session.commit() @@ -473,22 +496,25 @@ class UsesVisualizationMixin(UsesLibraryMixinItems): Mixin for controllers that use Visualization objects. """ + app: "StructuredApp" slug_builder = SlugBuilder() - def get_visualization_config(self, trans, visualization): + def get_visualization_config(self, trans: ProvidesUserContext, visualization): """Returns a visualization's configuration.""" latest_revision = visualization.latest_revision config = latest_revision.config return config - def get_hda_or_ldda(self, trans, hda_ldda, dataset_id): + def get_hda_or_ldda(self, trans: "GalaxyWebTransaction", hda_ldda, dataset_id): """Returns either HDA or LDDA for hda/ldda and id combination.""" if hda_ldda == "hda": return self.get_hda(trans, dataset_id, check_ownership=False, check_accessible=True) else: return self.get_library_dataset_dataset_association(trans, dataset_id) - def get_hda(self, trans, dataset_id, check_ownership=True, check_accessible=False, check_state=True): + def get_hda( + self, trans: "GalaxyWebTransaction", dataset_id, check_ownership=True, check_accessible=False, check_state=True + ): """ Get an HDA object by id performing security checks using the current transaction. @@ -528,7 +554,7 @@ class UsesVisualizationMixin(UsesLibraryMixinItems): ) return data - def _get_genome_data(self, trans, dataset, dbkey=None): + def _get_genome_data(self, trans: ProvidesHistoryContext, dataset, dbkey=None): """ Returns genome-wide data for dataset if available; if not, message is returned. """ @@ -551,7 +577,7 @@ class UsesVisualizationMixin(UsesLibraryMixinItems): if isinstance(dataset.datatype, ChromatinInteractions): source = "data" - data_provider = trans.app.data_provider_registry.get_data_provider( + data_provider = self.app.data_provider_registry.get_data_provider( trans, original_dataset=dataset, source=source ) # HACK: pass in additional params which are used for only some @@ -589,7 +615,7 @@ class UsesStoredWorkflowMixin(SharableItemSecurityMixin, UsesAnnotations): app: "StructuredApp" slug_builder = SlugBuilder() - def get_stored_workflow(self, trans, id, check_ownership=True, check_accessible=False): + def get_stored_workflow(self, trans: ProvidesUserContext, id, check_ownership=True, check_accessible=False): """Get a StoredWorkflow from the database by id, verifying ownership.""" # Load workflow from database workflow_contents_manager = workflows.WorkflowsManager(self.app) @@ -607,7 +633,7 @@ class UsesStoredWorkflowMixin(SharableItemSecurityMixin, UsesAnnotations): return workflow - def get_stored_workflow_steps(self, trans, stored_workflow: StoredWorkflow): + def get_stored_workflow_steps(self, trans: ProvidesHistoryContext, stored_workflow: StoredWorkflow): """Restores states for a stored workflow's steps.""" module_injector = WorkflowModuleInjector(trans) workflow = stored_workflow.latest_workflow @@ -618,7 +644,7 @@ class UsesStoredWorkflowMixin(SharableItemSecurityMixin, UsesAnnotations): except exceptions.ToolMissingException: pass - def _import_shared_workflow(self, trans, stored: StoredWorkflow): + def _import_shared_workflow(self, trans: ProvidesUserContext, stored: StoredWorkflow): """Imports a shared workflow""" # Copy workflow. imported_stored = StoredWorkflow() @@ -642,7 +668,7 @@ class UsesStoredWorkflowMixin(SharableItemSecurityMixin, UsesAnnotations): session.commit() return imported_stored - def _workflow_to_dict(self, trans, stored: StoredWorkflow) -> dict[str, Any]: + def _workflow_to_dict(self, trans: ProvidesHistoryContext, stored: StoredWorkflow) -> dict[str, Any]: """ Converts a workflow to a dict of attributes suitable for exporting. """ @@ -656,7 +682,7 @@ class UsesStoredWorkflowMixin(SharableItemSecurityMixin, UsesAnnotations): class UsesFormDefinitionsMixin: """Mixin for controllers that use Galaxy form objects.""" - def get_all_forms(self, trans, all_versions=False, filter=None, form_type="All"): + def get_all_forms(self, trans: ProvidesUserContext, all_versions=False, filter=None, form_type="All"): """ Return all the latest forms from the form_definition_current table if all_versions is set to True. Otherwise return all the versions @@ -673,7 +699,7 @@ class UsesFormDefinitionsMixin: else: return [fdc.latest_form for fdc in fdc_list if fdc.latest_form.type == form_type] - def save_widget_field(self, trans, field_obj, widget_name, **kwd): + def save_widget_field(self, trans: ProvidesUserContext, field_obj, widget_name, **kwd): # Save a form_builder field object params = util.Params(kwd) if isinstance(field_obj, trans.model.UserAddress): @@ -689,7 +715,7 @@ class UsesFormDefinitionsMixin: trans.sa_session.add(field_obj) trans.sa_session.commit() - def get_form_values(self, trans, user, form_definition, **kwd): + def get_form_values(self, trans: ProvidesUserContext, user, form_definition, **kwd): """ Returns the name:value dictionary containing all the form values """ @@ -699,6 +725,7 @@ class UsesFormDefinitionsMixin: field_type = field["type"] field_name = field["name"] input_value = params.get(field_name, "") + field_value: int | str | bool if field_type == AddressField.__name__: input_text_value = util.restore_text(input_value) if input_text_value == "new": @@ -736,7 +763,7 @@ class SharableMixin: @web.expose @web.require_login("modify Galaxy items") - def set_slug_async(self, trans, id, new_slug): + def set_slug_async(self, trans: "GalaxyWebTransaction", id, new_slug): item = self.get_item(trans, id) if item: # Only update slug if slug is not already in use. @@ -757,38 +784,60 @@ class SharableMixin: @web.expose @web.require_login("share Galaxy items") - def share(self, trans, id=None, email="", **kwd): + def share(self, trans: "GalaxyWebTransaction", id=None, email="", **kwd): """Handle sharing an item with a particular user.""" raise NotImplementedError() @web.expose - def display_by_username_and_slug(self, trans, username, slug, **kwargs): + def display_by_username_and_slug(self, trans: "GalaxyWebTransaction", username, slug, **kwargs): """Display item by username and slug.""" # Ensure slug is in the correct format. slug = slug.encode("latin1").decode("utf-8") self._display_by_username_and_slug(trans, username, slug, **kwargs) - def _display_by_username_and_slug(self, trans, username, slug, **kwargs): + def _display_by_username_and_slug(self, trans: "GalaxyWebTransaction", username, slug, **kwargs): raise NotImplementedError() - def get_item(self, trans, id): + def get_item(self, trans: "GalaxyWebTransaction", id): """Return item based on id.""" raise NotImplementedError() +class LooksUpTaggedItems(Protocol): + """What the tag helpers need from the object they run on. + + get_object comes from the BaseController subclass UsesTagsMixin is combined + with, _get_tagged_item from the mixin itself. + """ + + def get_object( + self, + trans: ProvidesUserContext, + id, + class_name, + check_ownership=False, + check_accessible=False, + deleted=None, + ): ... + + def _get_tagged_item(self, trans: ProvidesUserContext, item_class_name, id, check_ownership=True): ... + + class UsesTagsMixin(SharableItemSecurityMixin): - def _get_user_tags(self, trans, item_class_name, id): + def _get_user_tags(self: LooksUpTaggedItems, trans: ProvidesUserContext, item_class_name, id): user = trans.user tagged_item = self._get_tagged_item(trans, item_class_name, id) return [tag for tag in tagged_item.tags if tag.user == user] - def _get_tagged_item(self, trans, item_class_name, id, check_ownership=True): + def _get_tagged_item( + self: LooksUpTaggedItems, trans: ProvidesUserContext, item_class_name, id, check_ownership=True + ): tagged_item = self.get_object( trans, id, item_class_name, check_ownership=check_ownership, check_accessible=True ) return tagged_item - def _remove_items_tag(self, trans, item_class_name, id, tag_name): + def _remove_items_tag(self: LooksUpTaggedItems, trans: ProvidesUserContext, item_class_name, id, tag_name): """Remove a tag from an item.""" user = trans.user tagged_item = self._get_tagged_item(trans, item_class_name, id) @@ -796,27 +845,29 @@ class UsesTagsMixin(SharableItemSecurityMixin): trans.sa_session.commit() return deleted - def _apply_item_tag(self, trans, item_class_name, id, tag_name, tag_value=None): + def _apply_item_tag( + self: LooksUpTaggedItems, trans: ProvidesUserContext, item_class_name, id, tag_name, tag_value=None + ): user = trans.user tagged_item = self._get_tagged_item(trans, item_class_name, id) tag_assoc = trans.tag_handler.apply_item_tag(user, tagged_item, tag_name, tag_value) trans.sa_session.commit() return tag_assoc - def _get_item_tag_assoc(self, trans, item_class_name, id, tag_name): + def _get_item_tag_assoc(self: LooksUpTaggedItems, trans: ProvidesUserContext, item_class_name, id, tag_name): user = trans.user tagged_item = self._get_tagged_item(trans, item_class_name, id) log.debug(f"In get_item_tag_assoc with tagged_item {tagged_item}") return trans.tag_handler._get_item_tag_assoc(user, tagged_item, tag_name) - def set_tags_from_list(self, trans, item, new_tags_list, user=None): + def set_tags_from_list(self, trans: ProvidesUserContext, item, new_tags_list, user=None): return trans.tag_handler.set_tags_from_list(user, item, new_tags_list) class UsesExtendedMetadataMixin(SharableItemSecurityMixin): """Mixin for getting and setting item extended metadata.""" - def get_item_extended_metadata_obj(self, trans, item): + def get_item_extended_metadata_obj(self, trans: ProvidesUserContext, item): """ Given an item object (such as a LibraryDatasetDatasetAssociation), find the object of the associated extended metadata @@ -825,10 +876,10 @@ class UsesExtendedMetadataMixin(SharableItemSecurityMixin): return item.extended_metadata return None - def set_item_extended_metadata_obj(self, trans, item, extmeta_obj, check_writable=False): + def set_item_extended_metadata_obj(self, trans: ProvidesUserContext, item, extmeta_obj, check_writable=False): if item.__class__ == LibraryDatasetDatasetAssociation: if not check_writable or trans.app.security_agent.can_modify_library_item( - trans.get_current_user_roles(), item, trans.user + trans.get_current_user_roles(), item ): item.extended_metadata = extmeta_obj trans.sa_session.commit() @@ -842,10 +893,10 @@ class UsesExtendedMetadataMixin(SharableItemSecurityMixin): item.extended_metadata = extmeta_obj trans.sa_session.commit() - def unset_item_extended_metadata_obj(self, trans, item, check_writable=False): + def unset_item_extended_metadata_obj(self, trans: ProvidesUserContext, item, check_writable=False): if item.__class__ == LibraryDatasetDatasetAssociation: if not check_writable or trans.app.security_agent.can_modify_library_item( - trans.get_current_user_roles(), item, trans.user + trans.get_current_user_roles(), item ): item.extended_metadata = None trans.sa_session.commit() @@ -859,7 +910,7 @@ class UsesExtendedMetadataMixin(SharableItemSecurityMixin): item.extended_metadata = None trans.sa_session.commit() - def create_extended_metadata(self, trans, extmeta): + def create_extended_metadata(self, trans: ProvidesUserContext, extmeta): """ Create/index an extended metadata object. The returned object is not associated with any items @@ -873,7 +924,7 @@ class UsesExtendedMetadataMixin(SharableItemSecurityMixin): trans.sa_session.commit() return ex_meta - def delete_extended_metadata(self, trans, item): + def delete_extended_metadata(self, trans: ProvidesUserContext, item): if item.__class__ == ExtendedMetadata: trans.sa_session.delete(item) trans.sa_session.commit() diff --git a/lib/galaxy/webapps/base/webapp.py b/lib/galaxy/webapps/base/webapp.py index 8e58eee2e69..389b8e06384 100644 --- a/lib/galaxy/webapps/base/webapp.py +++ b/lib/galaxy/webapps/base/webapp.py @@ -199,7 +199,7 @@ class WebApplication(base.WebApplication): directories=paths, module_directory=galaxy_app.config.template_cache_path, collection_size=500 ) - def handle_controller_exception(self, e, trans, method, kwargs): + def handle_controller_exception(self, e, trans: "GalaxyWebTransaction", method, kwargs): if not isinstance(e, HTTPException): # We're still logging too much here but at least it's not logging webob.exc.HTTPFound and friends log.debug(f"Encountered exception in controller method: {method}", exc_info=True) @@ -221,7 +221,7 @@ class WebApplication(base.WebApplication): trans.response.status = e.status_code return trans.show_message(sanitize_html(e.err_msg), e.type) - def make_body_iterable(self, trans, body): + def make_body_iterable(self, trans: "base.DefaultWebTransaction", body): return base.WebApplication.make_body_iterable(self, trans, body) def transaction_chooser(self, environ, galaxy_app: BasicSharedApp, session_cookie: str): @@ -1128,7 +1128,7 @@ class GalaxyWebTransaction(base.DefaultWebTransaction, context.ProvidesHistoryCo return url_for(path, qualified=True) -def create_new_session(trans, prev_galaxy_session=None, user_for_new_session=None): +def create_new_session(trans: "GalaxyWebTransaction", prev_galaxy_session=None, user_for_new_session=None): """ Create a new GalaxySession for this request, possibly with a connection to a previous session (in `prev_galaxy_session`) and an existing user diff --git a/lib/galaxy/webapps/galaxy/api/agents.py b/lib/galaxy/webapps/galaxy/api/agents.py index 637f938c11d..246d7572f3e 100644 --- a/lib/galaxy/webapps/galaxy/api/agents.py +++ b/lib/galaxy/webapps/galaxy/api/agents.py @@ -30,6 +30,7 @@ from galaxy.webapps.galaxy.api import ( DependsOnUser, Router, ) +from galaxy.work.context import SessionRequestContext log = logging.getLogger(__name__) @@ -90,7 +91,7 @@ class AgentAPI: async def query_agent( self, request: AgentQueryRequest, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, user: User = DependsOnUser, ) -> AgentQueryResponse: """Query an AI agent. Use agent_type='auto' for automatic routing. @@ -129,7 +130,7 @@ class AgentAPI: job_id: DecodedDatabaseIdField | None = Body(None, description="Job ID for context"), error_details: dict[str, Any] | None = Body(None, description="Additional error details"), save_exchange: bool | None = Body(None, description="Save exchange for feedback tracking. Defaults to false."), - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, user: User = DependsOnUser, ) -> AgentResponse: """Analyze job errors and provide debugging assistance. @@ -181,7 +182,7 @@ class AgentAPI: query: str = Body(..., description="Description of the tool to create"), context: dict[str, Any] | None = Body(None, description="Additional context for tool creation"), save_exchange: bool | None = Body(None, description="Save exchange for feedback tracking. Defaults to false."), - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, user: User = DependsOnUser, ) -> AgentResponse: """Create a custom Galaxy tool. @@ -216,7 +217,7 @@ class AgentAPI: async def history_summary( self, history_id: str = Body(..., embed=True, description="Encoded id of the history to summarize."), - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, user: User = DependsOnUser, ) -> AgentResponse: """Produce a comprehensive markdown report for a history's analysis. diff --git a/lib/galaxy/webapps/galaxy/api/chat.py b/lib/galaxy/webapps/galaxy/api/chat.py index cef6d5ce473..54ad830846e 100644 --- a/lib/galaxy/webapps/galaxy/api/chat.py +++ b/lib/galaxy/webapps/galaxy/api/chat.py @@ -27,7 +27,9 @@ from galaxy.exceptions import ( ) from galaxy.managers.agents import AgentService from galaxy.managers.chat import ChatManager -from galaxy.managers.context import ProvidesUserContext +from galaxy.managers.context import ( + ProvidesUserContext, +) from galaxy.managers.jobs import JobManager from galaxy.managers.markdown_util import ready_galaxy_markdown_for_export from galaxy.managers.workflows import WorkflowsManager @@ -50,6 +52,7 @@ from galaxy.webapps.galaxy.api import ( DependsOnUser, Router, ) +from galaxy.work.context import SessionRequestContext # Import agent system try: @@ -129,7 +132,7 @@ class ChatAPI: payload: ChatPayload | None = None, query: str | None = Query(default=None, description="Query string for general chat"), agent_type: str = Query(default="auto", description="Agent type to use for the query"), - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, user: User = DependsOnUser, ) -> ChatResponse: """GalaxyAI endpoint - handles both job-based and general chat queries @@ -409,7 +412,7 @@ class ChatAPI: workflow_id: str = Path(..., description="Workflow ID to generate the report for"), version: int | None = Query(None, description="Version of the workflow"), instance: bool = Query(False, description="Whether the workflow_id is an instance ID"), - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, user: User = DependsOnUser, ) -> WorkflowReportResponse: """Generate a report for the specified workflow.""" @@ -563,7 +566,7 @@ class ChatAPI: self, query: str, agent_type: str, - trans: ProvidesUserContext, + trans: SessionRequestContext, user: User, job=None, context: dict[str, Any] | None = None, @@ -576,7 +579,7 @@ class ChatAPI: self, query: str, agent_type: str, - trans: ProvidesUserContext, + trans: SessionRequestContext, user: User, job=None, context: dict[str, Any] | None = None, diff --git a/lib/galaxy/webapps/galaxy/api/container_resolution.py b/lib/galaxy/webapps/galaxy/api/container_resolution.py index 348fa9b4378..7f1141e1c87 100644 --- a/lib/galaxy/webapps/galaxy/api/container_resolution.py +++ b/lib/galaxy/webapps/galaxy/api/container_resolution.py @@ -11,6 +11,7 @@ from galaxy.web import ( expose_api, require_admin, ) +from galaxy.webapps.base.webapp import GalaxyWebTransaction from . import BaseGalaxyAPIController log = logging.getLogger(__name__) @@ -23,7 +24,7 @@ class ContainerResolutionAPIController(BaseGalaxyAPIController): @expose_api @require_admin - def index(self, trans, **kwd): + def index(self, trans: GalaxyWebTransaction, **kwd): """ GET /api/container_resolvers """ @@ -31,7 +32,7 @@ class ContainerResolutionAPIController(BaseGalaxyAPIController): @expose_api @require_admin - def show(self, trans, index): + def show(self, trans: GalaxyWebTransaction, index): """ GET /api/container_resolvers/ """ @@ -39,7 +40,7 @@ class ContainerResolutionAPIController(BaseGalaxyAPIController): @expose_api @require_admin - def resolve(self, trans, index=None, **kwds): + def resolve(self, trans: GalaxyWebTransaction, index=None, **kwds): """ GET /api/container_resolvers/resolve GET /api/container_resolvers/{index}/resolve @@ -69,7 +70,7 @@ class ContainerResolutionAPIController(BaseGalaxyAPIController): @expose_api @require_admin - def resolve_toolbox(self, trans, **kwds): + def resolve_toolbox(self, trans: GalaxyWebTransaction, **kwds): """ GET /api/container_resolvers/toolbox GET /api/container_resolvers/{index}/toolbox @@ -89,7 +90,7 @@ class ContainerResolutionAPIController(BaseGalaxyAPIController): @expose_api @require_admin - def resolve_toolbox_with_install(self, trans, payload, **kwds): + def resolve_toolbox_with_install(self, trans: GalaxyWebTransaction, payload, **kwds): """ POST /api/container_resolvers/toolbox/install POST /api/container_resolvers/{index}/toolbox/install @@ -108,7 +109,7 @@ class ContainerResolutionAPIController(BaseGalaxyAPIController): @expose_api @require_admin - def resolve_with_install(self, trans, payload, **kwds): + def resolve_with_install(self, trans: GalaxyWebTransaction, payload, **kwds): """ POST /api/container_resolvers/resolve/install POST /api/container_resolvers/{index}/resolve/install diff --git a/lib/galaxy/webapps/galaxy/api/extended_metadata.py b/lib/galaxy/webapps/galaxy/api/extended_metadata.py index 78faa6fdcb1..fa39d761d26 100644 --- a/lib/galaxy/webapps/galaxy/api/extended_metadata.py +++ b/lib/galaxy/webapps/galaxy/api/extended_metadata.py @@ -18,6 +18,7 @@ from galaxy.webapps.base.controller import ( UsesLibraryMixinItems, UsesStoredWorkflowMixin, ) +from galaxy.webapps.base.webapp import GalaxyWebTransaction from . import ( BaseGalaxyAPIController, depends, @@ -33,10 +34,10 @@ class BaseExtendedMetadataController( ): exmeta_item_id: str - def _get_item_from_id(self, trans, idstr, check_writable=True) -> T | None: ... + def _get_item_from_id(self, trans: GalaxyWebTransaction, idstr, check_writable=True) -> T | None: ... @web.expose_api - def index(self, trans, **kwd): + def index(self, trans: GalaxyWebTransaction, **kwd): idnum = kwd[self.exmeta_item_id] item = self._get_item_from_id(trans, idnum, check_writable=False) if item is not None: @@ -45,7 +46,7 @@ class BaseExtendedMetadataController( return ex_meta.data @web.expose_api - def create(self, trans, payload, **kwd): + def create(self, trans: GalaxyWebTransaction, payload, **kwd): idnum = kwd[self.exmeta_item_id] item = self._get_item_from_id(trans, idnum, check_writable=True) if item is not None: @@ -61,7 +62,9 @@ class LibraryDatasetExtendMetadataController(BaseExtendedMetadataController[mode controller_name = "library_dataset_extended_metadata" exmeta_item_id = "library_content_id" - def _get_item_from_id(self, trans, idstr, check_writable=True) -> model.LibraryDatasetDatasetAssociation | None: + def _get_item_from_id( + self, trans: GalaxyWebTransaction, idstr, check_writable=True + ) -> model.LibraryDatasetDatasetAssociation | None: if check_writable: item = self.get_library_dataset_dataset_association(trans, idstr) if trans.app.security_agent.can_modify_library_item(trans.get_current_user_roles(), item): @@ -78,7 +81,9 @@ class HistoryDatasetExtendMetadataController(BaseExtendedMetadataController[mode exmeta_item_id = "history_content_id" hda_manager: managers.hdas.HDAManager = depends(managers.hdas.HDAManager) - def _get_item_from_id(self, trans, idstr, check_writable=True) -> model.HistoryDatasetAssociation | None: + def _get_item_from_id( + self, trans: GalaxyWebTransaction, idstr, check_writable=True + ) -> model.HistoryDatasetAssociation | None: decoded_idstr = self.decode_id(idstr) if check_writable: return self.hda_manager.get_owned(decoded_idstr, trans.user, current_history=trans.history) diff --git a/lib/galaxy/webapps/galaxy/api/folder_contents.py b/lib/galaxy/webapps/galaxy/api/folder_contents.py index dcc9094476d..a2df670ecb6 100644 --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -23,6 +23,7 @@ from galaxy.webapps.galaxy.api import ( ) from galaxy.webapps.galaxy.api.common import FolderIdPathParam from galaxy.webapps.galaxy.services.library_folder_contents import LibraryFolderContentsService +from galaxy.work.context import SessionRequestContext log = logging.getLogger(__name__) @@ -117,7 +118,7 @@ class FastAPILibraryFoldersContents: def create( self, folder_id: FolderIdPathParam, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, payload: CreateLibraryFilePayload = Body(...), ): return self.service.create(trans, folder_id, payload) diff --git a/lib/galaxy/webapps/galaxy/api/forms.py b/lib/galaxy/webapps/galaxy/api/forms.py index 8c019e0252f..34038c2cc50 100644 --- a/lib/galaxy/webapps/galaxy/api/forms.py +++ b/lib/galaxy/webapps/galaxy/api/forms.py @@ -16,6 +16,7 @@ from galaxy.model import FormDefinition from galaxy.schema.fields import DecodedDatabaseIdField from galaxy.util import XML from galaxy.webapps.base.controller import url_for +from galaxy.webapps.base.webapp import GalaxyWebTransaction from galaxy.webapps.galaxy.api import ( depends, DependsOnTrans, @@ -50,7 +51,7 @@ class FastAPIForms: class FormDefinitionAPIController(BaseGalaxyAPIController): @web.legacy_expose_api - def index(self, trans, **kwd): + def index(self, trans: GalaxyWebTransaction, **kwd): """ GET /api/forms Displays a collection (list) of forms. @@ -70,7 +71,7 @@ class FormDefinitionAPIController(BaseGalaxyAPIController): return rval @web.legacy_expose_api - def show(self, trans, id, **kwd): + def show(self, trans: GalaxyWebTransaction, id, **kwd): """ GET /api/forms/{encoded_form_id} Displays information about a form. @@ -96,7 +97,7 @@ class FormDefinitionAPIController(BaseGalaxyAPIController): return item @web.legacy_expose_api - def create(self, trans, payload, **kwd): + def create(self, trans: GalaxyWebTransaction, payload, **kwd): """ POST /api/forms Creates a new form. diff --git a/lib/galaxy/webapps/galaxy/api/genomes.py b/lib/galaxy/webapps/galaxy/api/genomes.py index 7c40664246f..7c8ebd85e86 100644 --- a/lib/galaxy/webapps/galaxy/api/genomes.py +++ b/lib/galaxy/webapps/galaxy/api/genomes.py @@ -10,6 +10,7 @@ from fastapi.responses import Response from galaxy.managers.context import ProvidesUserContext from galaxy.managers.genomes import GenomesManager +from galaxy.work.context import SessionRequestContext from . import ( depends, DependsOnTrans, @@ -82,7 +83,7 @@ class FastAPIGenomes: ) def show( self, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, id: str = IdPathParam, reference: bool = ReferenceQueryParam, num: int = NumQueryParam, diff --git a/lib/galaxy/webapps/galaxy/api/group_roles.py b/lib/galaxy/webapps/galaxy/api/group_roles.py index c7129e6b18e..80a633c251c 100644 --- a/lib/galaxy/webapps/galaxy/api/group_roles.py +++ b/lib/galaxy/webapps/galaxy/api/group_roles.py @@ -4,7 +4,6 @@ API operations on Group objects. import logging -from galaxy.managers.context import ProvidesAppContext from galaxy.managers.group_roles import GroupRolesManager from galaxy.model.db.role import get_private_role_user_emails_dict from galaxy.schema.fields import Security @@ -21,13 +20,16 @@ from galaxy.webapps.galaxy.api.common import ( GroupIDPathParam, RoleIDPathParam, ) +from galaxy.work.context import SessionRequestContext log = logging.getLogger(__name__) router = Router(tags=["group_roles"]) -def group_role_to_model(trans, group_id: int, role, displayed_name: str | None = None) -> GroupRoleResponse: +def group_role_to_model( + trans: SessionRequestContext, group_id: int, role, displayed_name: str | None = None +) -> GroupRoleResponse: encoded_group_id = Security.security.encode_id(group_id) encoded_role_id = Security.security.encode_id(role.id) url = trans.url_builder("group_role", group_id=encoded_group_id, role_id=encoded_role_id) @@ -48,7 +50,7 @@ class FastAPIGroupRoles: def index( self, group_id: GroupIDPathParam, - trans: ProvidesAppContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> GroupRoleListResponse: group_roles = self.manager.index(trans, group_id) role_ids = {gr.role.id for gr in group_roles} @@ -70,7 +72,7 @@ class FastAPIGroupRoles: self, group_id: GroupIDPathParam, role_id: RoleIDPathParam, - trans: ProvidesAppContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> GroupRoleResponse: role = self.manager.show(trans, role_id, group_id) return group_role_to_model(trans, group_id, role) @@ -80,7 +82,7 @@ class FastAPIGroupRoles: self, group_id: GroupIDPathParam, role_id: RoleIDPathParam, - trans: ProvidesAppContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> GroupRoleResponse: role = self.manager.update(trans, role_id, group_id) return group_role_to_model(trans, group_id, role) @@ -90,7 +92,7 @@ class FastAPIGroupRoles: self, group_id: GroupIDPathParam, role_id: RoleIDPathParam, - trans: ProvidesAppContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> GroupRoleResponse: role = self.manager.delete(trans, role_id, group_id) return group_role_to_model(trans, group_id, role) diff --git a/lib/galaxy/webapps/galaxy/api/group_users.py b/lib/galaxy/webapps/galaxy/api/group_users.py index aadf7c65730..717b4e8db4c 100644 --- a/lib/galaxy/webapps/galaxy/api/group_users.py +++ b/lib/galaxy/webapps/galaxy/api/group_users.py @@ -4,7 +4,6 @@ API operations on Group objects. import logging -from galaxy.managers.context import ProvidesAppContext from galaxy.managers.group_users import GroupUsersManager from galaxy.schema.fields import Security from galaxy.schema.schema import ( @@ -20,13 +19,14 @@ from galaxy.webapps.galaxy.api.common import ( GroupIDPathParam, UserIdPathParam, ) +from galaxy.work.context import SessionRequestContext log = logging.getLogger(__name__) router = Router(tags=["group_users"]) -def group_user_to_model(trans, group_id, user) -> GroupUserResponse: +def group_user_to_model(trans: SessionRequestContext, group_id, user) -> GroupUserResponse: encoded_group_id = Security.security.encode_id(group_id) encoded_user_id = Security.security.encode_id(user.id) url = trans.url_builder("group_user", group_id=encoded_group_id, user_id=encoded_user_id) @@ -46,7 +46,7 @@ class FastAPIGroupUsers: def index( self, group_id: GroupIDPathParam, - trans: ProvidesAppContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> GroupUserListResponse: """ GET /api/groups/{encoded_group_id}/users @@ -66,7 +66,7 @@ class FastAPIGroupUsers: self, group_id: GroupIDPathParam, user_id: UserIdPathParam, - trans: ProvidesAppContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> GroupUserResponse: """ Displays information about a group user. @@ -84,7 +84,7 @@ class FastAPIGroupUsers: self, group_id: GroupIDPathParam, user_id: UserIdPathParam, - trans: ProvidesAppContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> GroupUserResponse: """ PUT /api/groups/{encoded_group_id}/users/{encoded_user_id} @@ -103,7 +103,7 @@ class FastAPIGroupUsers: self, group_id: GroupIDPathParam, user_id: UserIdPathParam, - trans: ProvidesAppContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> GroupUserResponse: """ DELETE /api/groups/{encoded_group_id}/users/{encoded_user_id} diff --git a/lib/galaxy/webapps/galaxy/api/groups.py b/lib/galaxy/webapps/galaxy/api/groups.py index 126d8e65e8f..a5ffa7f238c 100644 --- a/lib/galaxy/webapps/galaxy/api/groups.py +++ b/lib/galaxy/webapps/galaxy/api/groups.py @@ -7,7 +7,6 @@ from typing import Annotated from fastapi import Body -from galaxy.managers.context import ProvidesAppContext from galaxy.managers.groups import GroupsManager from galaxy.schema.groups import ( GroupCreatePayload, @@ -21,6 +20,7 @@ from galaxy.webapps.galaxy.api import ( Router, ) from galaxy.webapps.galaxy.api.common import GroupIDPathParam +from galaxy.work.context import SessionRequestContext log = logging.getLogger(__name__) @@ -39,7 +39,7 @@ class FastAPIGroups: ) def index( self, - trans: ProvidesAppContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> GroupListResponse: return self.manager.index(trans) @@ -52,7 +52,7 @@ class FastAPIGroups: def create( self, payload: Annotated[GroupCreatePayload, Body(...)], - trans: ProvidesAppContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> GroupListResponse: return self.manager.create(trans, payload) @@ -65,7 +65,7 @@ class FastAPIGroups: def show( self, group_id: GroupIDPathParam, - trans: ProvidesAppContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> GroupResponse: return self.manager.show(trans, group_id) @@ -79,18 +79,18 @@ class FastAPIGroups: self, group_id: GroupIDPathParam, payload: Annotated[GroupUpdatePayload, Body(...)], - trans: ProvidesAppContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> GroupResponse: return self.manager.update(trans, group_id, payload) @router.delete("/api/groups/{group_id}", require_admin=True) - def delete(self, group_id: GroupIDPathParam, trans: ProvidesAppContext = DependsOnTrans): + def delete(self, group_id: GroupIDPathParam, trans: SessionRequestContext = DependsOnTrans): self.manager.delete(trans, group_id) @router.post("/api/groups/{group_id}/purge", require_admin=True) - def purge(self, group_id: GroupIDPathParam, trans: ProvidesAppContext = DependsOnTrans): + def purge(self, group_id: GroupIDPathParam, trans: SessionRequestContext = DependsOnTrans): self.manager.purge(trans, group_id) @router.post("/api/groups/{group_id}/undelete", require_admin=True) - def undelete(self, group_id: GroupIDPathParam, trans: ProvidesAppContext = DependsOnTrans): + def undelete(self, group_id: GroupIDPathParam, trans: SessionRequestContext = DependsOnTrans): self.manager.undelete(trans, group_id) diff --git a/lib/galaxy/webapps/galaxy/api/histories.py b/lib/galaxy/webapps/galaxy/api/histories.py index f38e16eb0c3..3f805607e6b 100644 --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -97,6 +97,7 @@ from galaxy.webapps.galaxy.api.common import ( ) from galaxy.webapps.galaxy.services.histories import HistoriesService from galaxy.webapps.galaxy.services.workflows import WorkflowsService +from galaxy.work.context import SessionRequestContext from .common import HistoryIDPathParam log = logging.getLogger(__name__) @@ -215,7 +216,7 @@ class FastAPIHistories: def index( self, response: Response, - trans: ProvidesHistoryContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, limit: int | None = LimitQueryParam, offset: int | None = OffsetQueryParam, show_own: bool = ShowOwnQueryParam, @@ -263,7 +264,7 @@ class FastAPIHistories: ) def count( self, - trans: ProvidesHistoryContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> int: return self.service.count(trans) @@ -274,7 +275,7 @@ class FastAPIHistories: ) def index_deleted( self, - trans: ProvidesHistoryContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, filter_query_params: FilterQueryParams = Depends(get_filter_query_params), serialization_params: SerializationParams = Depends(query_serialization_params), all: bool | None = AllHistoriesQueryParam, @@ -476,7 +477,7 @@ class FastAPIHistories: ) def create( self, - trans: ProvidesHistoryContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, payload: CreateHistoryPayload = Depends(CreateHistoryFormData.as_form), # type: ignore[attr-defined] payload_as_json: Any | None = Depends(try_get_request_body_as_json), serialization_params: SerializationParams = Depends(query_serialization_params), diff --git a/lib/galaxy/webapps/galaxy/api/history_contents.py b/lib/galaxy/webapps/galaxy/api/history_contents.py index 0f4d07f5255..7a9ebf863fe 100644 --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -1222,6 +1222,6 @@ class FastAPIHistoryContents: rval = self.service.materialize(trans, materialize_request) return rval - def _download_collection(self, trans, id): + def _download_collection(self, trans: ProvidesHistoryContext, id): archive = self.service.get_dataset_collection_archive_for_download(trans, id) return GalaxyStreamingResponse(archive.response(), headers=archive.get_headers()) diff --git a/lib/galaxy/webapps/galaxy/api/job_files.py b/lib/galaxy/webapps/galaxy/api/job_files.py index 805767488fb..205b0101e2b 100644 --- a/lib/galaxy/webapps/galaxy/api/job_files.py +++ b/lib/galaxy/webapps/galaxy/api/job_files.py @@ -153,7 +153,7 @@ class JobFilesAPIController(BaseGalaxyAPIController): return {"message": "ok"} @expose_api_anonymous_and_sessionless - def tus_patch(self, trans, **kwds): + def tus_patch(self, trans: ProvidesAppContext, **kwds): """ Exposed as PATCH /api/job_files/resumable_upload. diff --git a/lib/galaxy/webapps/galaxy/api/job_ports.py b/lib/galaxy/webapps/galaxy/api/job_ports.py index fd6693643a6..affa483a912 100644 --- a/lib/galaxy/webapps/galaxy/api/job_ports.py +++ b/lib/galaxy/webapps/galaxy/api/job_ports.py @@ -5,6 +5,7 @@ related to running and queued jobs. from galaxy.job_execution.ports import JobPortsView from galaxy.structured_app import StructuredApp from galaxy.web import expose_api_anonymous_and_sessionless +from galaxy.webapps.base.webapp import GalaxyWebTransaction from . import BaseGalaxyAPIController @@ -22,7 +23,7 @@ class JobPortsAPIController(BaseGalaxyAPIController): self._job_ports_view = JobPortsView(app) @expose_api_anonymous_and_sessionless - def create(self, trans, job_id, payload, **kwargs): + def create(self, trans: GalaxyWebTransaction, job_id, payload, **kwargs): """ create( self, trans, job_id, payload, **kwargs ) * POST /api/jobs/{job_id}/ports diff --git a/lib/galaxy/webapps/galaxy/api/job_tokens.py b/lib/galaxy/webapps/galaxy/api/job_tokens.py index 7bd4a9cdca9..b576b409605 100644 --- a/lib/galaxy/webapps/galaxy/api/job_tokens.py +++ b/lib/galaxy/webapps/galaxy/api/job_tokens.py @@ -51,7 +51,7 @@ class FastAPIJobTokens: tokens = job.user.get_oidc_tokens(provider_name_to_backend(provider)) return tokens["id"] - def __authorize_job_access(self, trans, encoded_job_id, job_key): + def __authorize_job_access(self, trans: ProvidesAppContext, encoded_job_id, job_key): session = trans.sa_session job_id = trans.security.decode_id(encoded_job_id) job = session.get(Job, job_id) diff --git a/lib/galaxy/webapps/galaxy/api/jobs.py b/lib/galaxy/webapps/galaxy/api/jobs.py index 322abe20c3a..b7e53720c51 100644 --- a/lib/galaxy/webapps/galaxy/api/jobs.py +++ b/lib/galaxy/webapps/galaxy/api/jobs.py @@ -60,6 +60,7 @@ from galaxy.schema.types import OffsetNaiveDatetime from galaxy.tool_util.output_checker import AnyJobMessage from galaxy.web import expose_api_anonymous from galaxy.webapps.base.controller import UsesVisualizationMixin +from galaxy.webapps.base.webapp import GalaxyWebTransaction from galaxy.webapps.galaxy.api import ( BaseGalaxyAPIController, depends, @@ -76,7 +77,10 @@ from galaxy.webapps.galaxy.services.jobs import ( JobRequest, JobsService, ) -from galaxy.work.context import proxy_work_context_for_history +from galaxy.work.context import ( + proxy_work_context_for_history, + SessionRequestContext, +) from .tools import validate_not_protected log = logging.getLogger(__name__) @@ -483,7 +487,7 @@ class FastAPIJobs: self, job_id: JobIdPathParam, hda_ldda: Annotated[DatasetSourceType | None, DeprecatedHdaLddaQueryParam] = DatasetSourceType.hda, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> JobDisplayParametersSummary: """Resolve parameters as a list for nested display.""" hda_ldda_str = hda_ldda or "hda" @@ -501,7 +505,7 @@ class FastAPIJobs: self, dataset_id: DatasetIdPathParam, hda_ldda: Annotated[DatasetSourceType, HdaLddaQueryParam] = DatasetSourceType.hda, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> JobDisplayParametersSummary: """Resolve parameters as a list for nested display.""" job = self.service.get_job(trans, dataset_id=dataset_id, hda_ldda=hda_ldda) @@ -666,7 +670,7 @@ class JobController(BaseGalaxyAPIController, UsesVisualizationMixin): job_manager = depends(JobManager) @expose_api_anonymous - def build_for_rerun(self, trans: ProvidesHistoryContext, id, **kwd): + def build_for_rerun(self, trans: GalaxyWebTransaction, id, **kwd): """ * GET /api/jobs/{id}/build_for_rerun returns a tool input/param template prepopulated with this job's @@ -692,7 +696,7 @@ class JobController(BaseGalaxyAPIController, UsesVisualizationMixin): raise exceptions.ConfigDoesNotAllowException(f"Tool '{job.tool_id}' cannot be rerun.") return tool.to_json(trans, {}, job=job) - def __get_job(self, trans, job_id=None, dataset_id=None, **kwd): + def __get_job(self, trans: GalaxyWebTransaction, job_id=None, dataset_id=None, **kwd): if job_id is not None: decoded_job_id = self.decode_id(job_id) return self.job_manager.get_accessible_job(trans, decoded_job_id) diff --git a/lib/galaxy/webapps/galaxy/api/library_datasets.py b/lib/galaxy/webapps/galaxy/api/library_datasets.py index 78177458b14..9ce32833d4b 100644 --- a/lib/galaxy/webapps/galaxy/api/library_datasets.py +++ b/lib/galaxy/webapps/galaxy/api/library_datasets.py @@ -26,6 +26,11 @@ from galaxy.managers import ( library_datasets, roles, ) +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.model import DatasetPermissions from galaxy.model.db.role import get_private_role_user_emails_dict from galaxy.structured_app import StructuredApp @@ -43,6 +48,7 @@ from galaxy.web import ( expose_api_anonymous, ) from galaxy.webapps.base.controller import UsesVisualizationMixin +from galaxy.webapps.base.webapp import GalaxyWebTransaction from . import BaseGalaxyAPIController log = logging.getLogger(__name__) @@ -58,7 +64,7 @@ class LibraryDatasetsController(BaseGalaxyAPIController, UsesVisualizationMixin, self.ldda_manager = lddas.LDDAManager(app) @expose_api_anonymous - def show(self, trans, id, **kwd): + def show(self, trans: ProvidesUserContext, id, **kwd): """ GET /api/libraries/datasets/{encoded_dataset_id} @@ -75,7 +81,7 @@ class LibraryDatasetsController(BaseGalaxyAPIController, UsesVisualizationMixin, return serialized @expose_api_anonymous - def show_version(self, trans, encoded_dataset_id, encoded_ldda_id, **kwd): + def show_version(self, trans: ProvidesUserContext, encoded_dataset_id, encoded_ldda_id, **kwd): """ GET /api/libraries/datasets/{encoded_dataset_id}/versions/{encoded_ldda_id} @@ -108,7 +114,7 @@ class LibraryDatasetsController(BaseGalaxyAPIController, UsesVisualizationMixin, return rval @expose_api - def show_roles(self, trans, encoded_dataset_id, **kwd): + def show_roles(self, trans: ProvidesUserContext, encoded_dataset_id, **kwd): """ GET /api/libraries/datasets/{encoded_dataset_id}/permissions @@ -164,7 +170,7 @@ class LibraryDatasetsController(BaseGalaxyAPIController, UsesVisualizationMixin, "The value of 'scope' parameter is invalid. Alllowed values: current, available" ) - def _get_current_roles(self, trans, library_dataset): + def _get_current_roles(self, trans: ProvidesAppContext, library_dataset): """ Find all roles currently connected to relevant permissions on the library dataset and the underlying dataset. @@ -178,7 +184,7 @@ class LibraryDatasetsController(BaseGalaxyAPIController, UsesVisualizationMixin, return self.ldda_manager.serialize_dataset_association_roles(library_dataset) @expose_api - def update(self, trans, encoded_dataset_id, payload=None, **kwd): + def update(self, trans: ProvidesUserContext, encoded_dataset_id, payload=None, **kwd): """ PATCH /api/libraries/datasets/{encoded_dataset_id} @@ -209,7 +215,7 @@ class LibraryDatasetsController(BaseGalaxyAPIController, UsesVisualizationMixin, return serialized @expose_api - def update_permissions(self, trans, encoded_dataset_id, payload=None, **kwd): + def update_permissions(self, trans: ProvidesUserContext, encoded_dataset_id, payload=None, **kwd): """ POST /api/libraries/datasets/{encoded_dataset_id}/permissions @@ -332,7 +338,7 @@ class LibraryDatasetsController(BaseGalaxyAPIController, UsesVisualizationMixin, return self._get_current_roles(trans, library_dataset) @expose_api - def delete(self, trans, encoded_dataset_id, **kwd): + def delete(self, trans: ProvidesUserContext, encoded_dataset_id, **kwd): """ DELETE /api/libraries/datasets/{encoded_dataset_id} @@ -374,7 +380,7 @@ class LibraryDatasetsController(BaseGalaxyAPIController, UsesVisualizationMixin, return rval @expose_api - def load(self, trans, payload=None, **kwd): + def load(self, trans: ProvidesHistoryContext, payload=None, **kwd): """ POST /api/libraries/datasets @@ -524,6 +530,7 @@ class LibraryDatasetsController(BaseGalaxyAPIController, UsesVisualizationMixin, # Set up the traditional tool state/params tool_id = "upload1" tool = trans.app.toolbox.get_tool(tool_id) + assert tool is not None, f"'{tool_id}' tool not found in toolbox" state = tool.new_state(trans) populate_state(trans, tool.inputs, kwd, state.inputs) tool_params = state.inputs @@ -578,7 +585,7 @@ class LibraryDatasetsController(BaseGalaxyAPIController, UsesVisualizationMixin, @web.expose # TODO convert to expose_api - def download(self, trans, archive_format, **kwd): + def download(self, trans: GalaxyWebTransaction, archive_format, **kwd): """ GET /api/libraries/datasets/download/{archive_format} POST /api/libraries/datasets/download/{archive_format} diff --git a/lib/galaxy/webapps/galaxy/api/page_revisions.py b/lib/galaxy/webapps/galaxy/api/page_revisions.py index b07797bc521..43d3e7c3900 100644 --- a/lib/galaxy/webapps/galaxy/api/page_revisions.py +++ b/lib/galaxy/webapps/galaxy/api/page_revisions.py @@ -10,6 +10,7 @@ from galaxy.managers.pages import ( PageManager, ) from galaxy.web import expose_api +from galaxy.webapps.base.webapp import GalaxyWebTransaction from . import ( BaseGalaxyAPIController, depends, @@ -22,7 +23,7 @@ class PageRevisionsController(BaseGalaxyAPIController): manager: PageManager = depends(PageManager) @expose_api - def index(self, trans, page_id, **kwd): + def index(self, trans: GalaxyWebTransaction, page_id, **kwd): """ index( self, trans, page_id, **kwd ) * GET /api/pages/{page_id}/revisions @@ -43,7 +44,7 @@ class PageRevisionsController(BaseGalaxyAPIController): return out @expose_api - def create(self, trans, page_id, payload, **kwd): + def create(self, trans: GalaxyWebTransaction, page_id, payload, **kwd): """ create( self, trans, page_id, payload **kwd ) * POST /api/pages/{page_id}/revisions diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py index d01c86bfc08..520aa834caa 100644 --- a/lib/galaxy/webapps/galaxy/api/pages.py +++ b/lib/galaxy/webapps/galaxy/api/pages.py @@ -40,6 +40,7 @@ from galaxy.webapps.galaxy.api import ( ) from galaxy.webapps.galaxy.api.common import PageIdPathParam from galaxy.webapps.galaxy.services.pages import PagesService +from galaxy.work.context import SessionRequestContext log = logging.getLogger(__name__) @@ -160,7 +161,7 @@ class FastAPIPages: ) def create( self, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, payload: CreatePagePayload = Body(...), ) -> PageDetails: """Creates a new Page.""" @@ -209,7 +210,7 @@ class FastAPIPages: def show_pdf( self, id: PageIdPathParam, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ): """Return a PDF document of the last revision of the Page. @@ -231,7 +232,7 @@ class FastAPIPages: def prepare_pdf( self, id: PageIdPathParam, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> AsyncFile: """Return a STS download link for this page to be downloaded as a PDF. @@ -247,7 +248,7 @@ class FastAPIPages: def show( self, id: PageIdPathParam, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> PageDetails: """Return summary information about a specific Page and the content of the last revision.""" return self.service.show(trans, id) @@ -348,7 +349,7 @@ class FastAPIPages: def update( self, id: PageIdPathParam, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, payload: UpdatePagePayload = Body(...), ) -> PageDetails: """Updates an existing Page.""" @@ -379,7 +380,7 @@ class FastAPIPages: self, id: PageIdPathParam, revision_id: PageIdRevisionPathParam, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> PageRevisionDetails: """Return the details of a specific page revision.""" return self.service.show_revision(trans, id, revision_id) @@ -392,7 +393,7 @@ class FastAPIPages: self, id: PageIdPathParam, revision_id: PageIdRevisionPathParam, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> PageRevisionDetails: """Restore a page to the content of a specific revision.""" return self.service.revert_revision(trans, id, revision_id) diff --git a/lib/galaxy/webapps/galaxy/api/provenance.py b/lib/galaxy/webapps/galaxy/api/provenance.py index b1eeaf0b0f2..ccb3bc38b51 100644 --- a/lib/galaxy/webapps/galaxy/api/provenance.py +++ b/lib/galaxy/webapps/galaxy/api/provenance.py @@ -12,6 +12,8 @@ from paste.httpexceptions import ( from galaxy import web from galaxy.managers.hdas import HDAManager from galaxy.util import string_as_bool +from galaxy.webapps.base.controller import SharableItemSecurityMixin +from galaxy.webapps.base.webapp import GalaxyWebTransaction from . import ( BaseGalaxyAPIController, depends, @@ -20,28 +22,32 @@ from . import ( log = logging.getLogger(__name__) -class BaseProvenanceController(BaseGalaxyAPIController): +class BaseProvenanceController(BaseGalaxyAPIController, SharableItemSecurityMixin): """ """ + provenance_item_class: str + provenance_item_id: str + hda_manager: HDAManager + @web.legacy_expose_api - def index(self, trans, **kwd): + def index(self, trans: GalaxyWebTransaction, **kwd): follow = string_as_bool(kwd.get("follow", False)) value = self._get_provenance(trans, self.provenance_item_class, kwd[self.provenance_item_id], follow) return value @web.legacy_expose_api - def show(self, trans, elem_name, **kwd): + def show(self, trans: GalaxyWebTransaction, elem_name, **kwd): raise HTTPNotImplemented() @web.legacy_expose_api - def create(self, trans, tag_name, payload=None, **kwd): + def create(self, trans: GalaxyWebTransaction, tag_name, payload=None, **kwd): raise HTTPNotImplemented() @web.legacy_expose_api - def delete(self, trans, tag_name, **kwd): + def delete(self, trans: GalaxyWebTransaction, tag_name, **kwd): raise HTTPBadRequest("Cannot Delete Provenance") - def _get_provenance(self, trans, item_class_name, item_id, follow=True): + def _get_provenance(self, trans: GalaxyWebTransaction, item_class_name, item_id, follow=True): provenance_item = self.get_object( trans, item_id, item_class_name, check_ownership=False, check_accessible=False ) @@ -52,7 +58,7 @@ class BaseProvenanceController(BaseGalaxyAPIController): out = self._get_record(trans, provenance_item, follow) return out - def _get_record(self, trans, item, follow): + def _get_record(self, trans: GalaxyWebTransaction, item, follow): if item is not None: if item.copied_from_library_dataset_dataset_association: item = item.copied_from_library_dataset_dataset_association @@ -74,7 +80,7 @@ class BaseProvenanceController(BaseGalaxyAPIController): } return None - def _get_job_record(self, trans, job, follow): + def _get_job_record(self, trans: GalaxyWebTransaction, job, follow): out = {} for p in job.parameters: out[p.name] = p.value diff --git a/lib/galaxy/webapps/galaxy/api/sanitize_allow.py b/lib/galaxy/webapps/galaxy/api/sanitize_allow.py index 1101462299c..9f8145a16a7 100755 --- a/lib/galaxy/webapps/galaxy/api/sanitize_allow.py +++ b/lib/galaxy/webapps/galaxy/api/sanitize_allow.py @@ -9,6 +9,7 @@ from typing import ( from galaxy import web from galaxy.webapps.base.controller import BaseAPIController +from galaxy.webapps.base.webapp import GalaxyWebTransaction log = logging.getLogger(__name__) @@ -16,7 +17,7 @@ log = logging.getLogger(__name__) class SanitizeAllowController(BaseAPIController): @web.require_admin @web.expose_api - def index(self, trans, **kwd): + def index(self, trans: GalaxyWebTransaction, **kwd): """ GET /api/sanitize_allow Return an object showing the current state of the toolbox and allow list. @@ -25,7 +26,7 @@ class SanitizeAllowController(BaseAPIController): @web.require_admin @web.expose_api - def create(self, trans, tool_id, **kwd): + def create(self, trans: GalaxyWebTransaction, tool_id, **kwd): """ PUT /api/sanitize_allow Add a new tool_id to the allowlist. @@ -37,7 +38,7 @@ class SanitizeAllowController(BaseAPIController): @web.require_admin @web.expose_api - def delete(self, trans, tool_id, **kwd): + def delete(self, trans: GalaxyWebTransaction, tool_id, **kwd): """ DELETE /api/sanitize_allow Remove tool_id from allowlist. @@ -47,13 +48,13 @@ class SanitizeAllowController(BaseAPIController): self._save_allowlist(trans) return self._generate_allowlist(trans) - def _save_allowlist(self, trans): + def _save_allowlist(self, trans: GalaxyWebTransaction): trans.app.config.sanitize_allowlist = sorted(trans.app.config.sanitize_allowlist) with open(trans.app.config.sanitize_allowlist_file, "w") as f: f.write("\n".join(trans.app.config.sanitize_allowlist)) trans.app.queue_worker.send_control_task("reload_sanitize_allowlist", noop_self=True) - def _generate_allowlist(self, trans): + def _generate_allowlist(self, trans: GalaxyWebTransaction): sanitize_dict: dict[str, Any] = dict( blocked_toolshed=[], allowed_toolshed=[], blocked_local=[], allowed_local=[] ) diff --git a/lib/galaxy/webapps/galaxy/api/tool_dependencies.py b/lib/galaxy/webapps/galaxy/api/tool_dependencies.py index 70fb93b3065..3ac527d17ca 100644 --- a/lib/galaxy/webapps/galaxy/api/tool_dependencies.py +++ b/lib/galaxy/webapps/galaxy/api/tool_dependencies.py @@ -39,7 +39,7 @@ class ToolDependenciesAPIController(BaseGalaxyAPIController): @require_admin @expose_api - def update(self, trans): + def update(self, trans: ProvidesAppContext): """ PUT /api/dependency_resolvers diff --git a/lib/galaxy/webapps/galaxy/api/tool_entry_points.py b/lib/galaxy/webapps/galaxy/api/tool_entry_points.py index 825517c0b19..0fe0173f122 100644 --- a/lib/galaxy/webapps/galaxy/api/tool_entry_points.py +++ b/lib/galaxy/webapps/galaxy/api/tool_entry_points.py @@ -8,7 +8,6 @@ from galaxy import ( exceptions, util, ) -from galaxy.managers.context import ProvidesUserContext from galaxy.model import ( InteractiveToolEntryPoint, Job, @@ -16,6 +15,7 @@ from galaxy.model import ( from galaxy.security.idencoding import IdAsLowercaseAlphanumEncodingHelper from galaxy.structured_app import StructuredApp from galaxy.web import expose_api_anonymous_and_sessionless +from galaxy.webapps.base.webapp import GalaxyWebTransaction from . import BaseGalaxyAPIController log = logging.getLogger(__name__) @@ -27,7 +27,7 @@ class ToolEntryPointsAPIController(BaseGalaxyAPIController): self.interactivetool_manager = app.interactivetool_manager @expose_api_anonymous_and_sessionless - def index(self, trans: ProvidesUserContext, running=False, job_id=None, **kwd): + def index(self, trans: GalaxyWebTransaction, running=False, job_id=None, **kwd): """ * GET /api/entry_points Returns tool entry point information. Currently passing a job_id @@ -75,7 +75,7 @@ class ToolEntryPointsAPIController(BaseGalaxyAPIController): return rval @expose_api_anonymous_and_sessionless - def access_entry_point(self, trans: ProvidesUserContext, id, **kwd): + def access_entry_point(self, trans: GalaxyWebTransaction, id, **kwd): """ * GET /api/entry_points/{id}/access Return the URL target described by the entry point. @@ -94,7 +94,7 @@ class ToolEntryPointsAPIController(BaseGalaxyAPIController): return {"target": self.interactivetool_manager.access_entry_point_target(trans, entry_point_id)} @expose_api_anonymous_and_sessionless - def stop_entry_point(self, trans: ProvidesUserContext, id, **kwds): + def stop_entry_point(self, trans: GalaxyWebTransaction, id, **kwds): """ DELETE /api/entry_points/{id} """ diff --git a/lib/galaxy/webapps/galaxy/api/tool_shed_repositories.py b/lib/galaxy/webapps/galaxy/api/tool_shed_repositories.py index ea823ec9664..a89aead79a7 100644 --- a/lib/galaxy/webapps/galaxy/api/tool_shed_repositories.py +++ b/lib/galaxy/webapps/galaxy/api/tool_shed_repositories.py @@ -3,6 +3,7 @@ import logging from time import strftime from typing import ( Annotated, + Any, ) from fastapi import ( @@ -18,7 +19,10 @@ from galaxy import ( exceptions, util, ) -from galaxy.managers.context import ProvidesUserContext +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesUserContext, +) from galaxy.schema.fields import DecodedDatabaseIdField from galaxy.schema.schema import ( CheckForUpdatesResponse, @@ -71,7 +75,7 @@ class ToolShedRepositoriesController(BaseGalaxyAPIController): service: ToolShedRepositoriesService = depends(ToolShedRepositoriesService) - def __ensure_can_install_repos(self, trans): + def __ensure_can_install_repos(self, trans: ProvidesUserContext): # Make sure this Galaxy instance is configured with a shed-related tool panel configuration file. if not have_shed_tool_conf_for_install(self.app): message = get_message_for_no_shed_tool_config() @@ -146,7 +150,7 @@ class ToolShedRepositoriesController(BaseGalaxyAPIController): @require_admin @expose_api - def install_repository_revisions(self, trans, payload, **kwd): + def install_repository_revisions(self, trans: ProvidesUserContext, payload, **kwd): """ POST /api/tool_shed_repositories/install_repository_revisions Install one or more specified repository revisions from one or more specified tool sheds into Galaxy. The received parameters @@ -245,7 +249,7 @@ class ToolShedRepositoriesController(BaseGalaxyAPIController): @require_admin @expose_api - def uninstall_repository(self, trans, id=None, **kwd): + def uninstall_repository(self, trans: ProvidesAppContext, id=None, **kwd): """ DELETE /api/tool_shed_repositories/id DELETE /api/tool_shed_repositories/ @@ -267,9 +271,9 @@ class ToolShedRepositoriesController(BaseGalaxyAPIController): except ValueError: raise HTTPBadRequest(detail=f"No repository with id '{id}' found") else: - tsr_arguments = ["name", "owner", "changeset_revision", "tool_shed_url"] + tsr_argument_names = ["name", "owner", "changeset_revision", "tool_shed_url"] try: - tsr_arguments = {key: kwd[key] for key in tsr_arguments} + tsr_arguments = {key: kwd[key] for key in tsr_argument_names} except KeyError as e: raise HTTPBadRequest(detail=f"Missing required parameter '{e.args[0]}'") repository = get_installed_repository( @@ -313,7 +317,7 @@ class ToolShedRepositoriesController(BaseGalaxyAPIController): @require_admin @expose_api - def reset_metadata_on_selected_installed_repositories(self, trans, **kwd): + def reset_metadata_on_selected_installed_repositories(self, trans: ProvidesAppContext, **kwd): if repository_ids := util.listify(kwd.get("repository_ids")): irmm = InstalledRepositoryMetadataManager(self.app) failed = [] @@ -340,7 +344,7 @@ class ToolShedRepositoriesController(BaseGalaxyAPIController): raise exceptions.MessageException("Please specify repository ids [repository_ids].") @expose_api - def reset_metadata_on_installed_repositories(self, trans, payload, **kwd): + def reset_metadata_on_installed_repositories(self, trans: ProvidesUserContext, payload, **kwd): """ PUT /api/tool_shed_repositories/reset_metadata_on_installed_repositories @@ -349,7 +353,9 @@ class ToolShedRepositoriesController(BaseGalaxyAPIController): :param key: the API key of the Galaxy admin user. """ start_time = strftime("%Y-%m-%d %H:%M:%S") - results = dict(start_time=start_time, successful_count=0, unsuccessful_count=0, repository_status=[]) + results: dict[str, Any] = dict( + start_time=start_time, successful_count=0, unsuccessful_count=0, repository_status=[] + ) # Make sure the current user's API key proves he is an admin user in this Galaxy instance. if not trans.user_is_admin: raise HTTPForbidden( diff --git a/lib/galaxy/webapps/galaxy/api/toolshed.py b/lib/galaxy/webapps/galaxy/api/toolshed.py index 7cf739116bc..80fd1bc06a2 100644 --- a/lib/galaxy/webapps/galaxy/api/toolshed.py +++ b/lib/galaxy/webapps/galaxy/api/toolshed.py @@ -8,6 +8,7 @@ from galaxy.web import ( expose_api, require_admin, ) +from galaxy.webapps.base.webapp import GalaxyWebTransaction from . import BaseGalaxyAPIController log = logging.getLogger(__name__) @@ -17,7 +18,7 @@ class ToolShedController(BaseGalaxyAPIController): """RESTful controller for interactions with Toolsheds.""" @expose_api - def index(self, trans, **kwd): + def index(self, trans: GalaxyWebTransaction, **kwd): """ GET /api/tool_shed Interact with the Toolshed registry of this instance. @@ -29,7 +30,7 @@ class ToolShedController(BaseGalaxyAPIController): @require_admin @expose_api - def request(self, trans, **params): + def request(self, trans: GalaxyWebTransaction, **params): """ GET /api/tool_shed/request """ diff --git a/lib/galaxy/webapps/galaxy/api/tours.py b/lib/galaxy/webapps/galaxy/api/tours.py index 647e16ba9ae..6561579de45 100644 --- a/lib/galaxy/webapps/galaxy/api/tours.py +++ b/lib/galaxy/webapps/galaxy/api/tours.py @@ -4,7 +4,6 @@ API Controller providing Galaxy Tours import logging -from galaxy.managers.context import ProvidesAppContext from galaxy.managers.tours import ToursManager from galaxy.schema.schema import GenerateTourResponse from galaxy.schema.tours import ( @@ -13,6 +12,7 @@ from galaxy.schema.tours import ( ) from galaxy.tours import ToursRegistry from galaxy.webapps.galaxy.api import DependsOnTrans +from galaxy.work.context import SessionRequestContext from . import ( depends, Router, @@ -36,7 +36,11 @@ class FastAPITours: @router.get("/api/tours/generate", public=True) def generate_tour( - self, tool_id: str, tool_version: str, performs_upload: bool = True, trans: ProvidesAppContext = DependsOnTrans + self, + tool_id: str, + tool_version: str, + performs_upload: bool = True, + trans: SessionRequestContext = DependsOnTrans, ) -> GenerateTourResponse: """Generate a tour designed for the given tool.""" return self.manager.generate_tour(tool_id, tool_version, trans, performs_upload=performs_upload) diff --git a/lib/galaxy/webapps/galaxy/api/trs_consumer.py b/lib/galaxy/webapps/galaxy/api/trs_consumer.py index d712f680667..c342acbf947 100644 --- a/lib/galaxy/webapps/galaxy/api/trs_consumer.py +++ b/lib/galaxy/webapps/galaxy/api/trs_consumer.py @@ -4,6 +4,7 @@ Information on TRS can be found at https://github.com/ga4gh/tool-registry-servic """ from galaxy.web import expose_api +from galaxy.webapps.base.webapp import GalaxyWebTransaction from galaxy.workflow.trs_proxy import TrsProxy from . import ( BaseGalaxyAPIController, @@ -17,17 +18,17 @@ class TrsConsumeAPIController(BaseGalaxyAPIController): _trs_proxy: TrsProxy = depends(TrsProxy) @expose_api - def get_servers(self, trans, *args, **kwd): + def get_servers(self, trans: GalaxyWebTransaction, *args, **kwd): return self._trs_proxy.get_servers() @expose_api - def get_tool(self, trans, *args, **kwd): + def get_tool(self, trans: GalaxyWebTransaction, *args, **kwd): return self._trs_proxy.get_server(kwd.pop("trs_server")).get_tool(**kwd) @expose_api - def get_versions(self, trans, *args, **kwd): + def get_versions(self, trans: GalaxyWebTransaction, *args, **kwd): return self._trs_proxy.get_server(kwd.pop("trs_server")).get_versions(**kwd) @expose_api - def get_version(self, trans, *args, **kwd): + def get_version(self, trans: GalaxyWebTransaction, *args, **kwd): return self._trs_proxy.get_server(kwd.pop("trs_server")).get_version(**kwd) diff --git a/lib/galaxy/webapps/galaxy/api/trs_search.py b/lib/galaxy/webapps/galaxy/api/trs_search.py index b8ad2dcbf45..8ea94df5277 100644 --- a/lib/galaxy/webapps/galaxy/api/trs_search.py +++ b/lib/galaxy/webapps/galaxy/api/trs_search.py @@ -6,6 +6,7 @@ Information on TRS can be found at https://github.com/ga4gh/tool-registry-servic import logging from galaxy.web import expose_api +from galaxy.webapps.base.webapp import GalaxyWebTransaction from galaxy.workflow.trs_proxy import ( parse_search_kwds, TrsProxy, @@ -28,7 +29,7 @@ class TrsSearchAPIController(BaseGalaxyAPIController): _trs_proxy: TrsProxy = depends(TrsProxy) @expose_api - def index(self, trans, trs_server=None, query=None, **kwd): + def index(self, trans: GalaxyWebTransaction, trs_server=None, query=None, **kwd): """ GET /api/trs_search diff --git a/lib/galaxy/webapps/galaxy/api/uploads.py b/lib/galaxy/webapps/galaxy/api/uploads.py index e98b1afcb55..cc9cf32ebdf 100644 --- a/lib/galaxy/webapps/galaxy/api/uploads.py +++ b/lib/galaxy/webapps/galaxy/api/uploads.py @@ -5,6 +5,7 @@ API operations for uploaded files in storage. import logging from galaxy.web.framework.decorators import expose_api_anonymous +from galaxy.webapps.base.webapp import GalaxyWebTransaction from . import BaseGalaxyAPIController log = logging.getLogger(__name__) @@ -14,7 +15,7 @@ class UploadsAPIController(BaseGalaxyAPIController): READ_CHUNK_SIZE = 2**16 @expose_api_anonymous - def hooks(self, trans, **kwds): + def hooks(self, trans: GalaxyWebTransaction, **kwds): """ Exposed as POST /api/upload/hooks and /api/upload/resumable_upload """ diff --git a/lib/galaxy/webapps/galaxy/api/users.py b/lib/galaxy/webapps/galaxy/api/users.py index f936a77c330..41adfe4470b 100644 --- a/lib/galaxy/webapps/galaxy/api/users.py +++ b/lib/galaxy/webapps/galaxy/api/users.py @@ -27,6 +27,7 @@ from galaxy import ( from galaxy.exceptions import ObjectInvalid from galaxy.managers import users from galaxy.managers.context import ( + ProvidesAppContext, ProvidesHistoryContext, ProvidesUserContext, ) @@ -83,6 +84,7 @@ from galaxy.webapps.base.controller import ( UsesFormDefinitionsMixin, UsesTagsMixin, ) +from galaxy.webapps.base.webapp import GalaxyWebTransaction from galaxy.webapps.galaxy.api import ( BaseGalaxyAPIController, depends, @@ -742,20 +744,20 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController service: UsersService = depends(UsersService) user_manager: users.UserManager = depends(users.UserManager) - def _get_user_full(self, trans, user_id, **kwd): + def _get_user_full(self, trans: ProvidesUserContext, user_id, **kwd): """Return referenced user or None if anonymous user is referenced.""" deleted = kwd.get("deleted", "False") deleted = util.string_as_bool(deleted) return self.service.get_user_full(trans, user_id, deleted) - def _get_extra_user_preferences(self, trans): + def _get_extra_user_preferences(self, trans: ProvidesAppContext): """ Reads the file user_preferences_extra_conf.yml to display admin defined user informations """ return trans.app.config.user_preferences_extra["preferences"] - def _build_extra_user_pref_inputs(self, trans, preferences, user): + def _build_extra_user_pref_inputs(self, trans: ProvidesAppContext, preferences, user): """ Build extra user preferences inputs list. Add values to the fields if present @@ -800,7 +802,7 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController return extra_pref_inputs @expose_api - def get_information(self, trans, id, **kwd): + def get_information(self, trans: GalaxyWebTransaction, id, **kwd): """ GET /api/users/{id}/information/inputs Return user details such as username, email, addresses etc. @@ -854,7 +856,7 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController ) if info_form_models: info_form_id = trans.security.encode_id(user.values.form_definition.id) if user.values else None - info_field = { + info_field: dict[str, Any] = { "type": "conditional", "name": "info", "cases": [], @@ -880,7 +882,7 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController address_inputs = [{"type": "hidden", "name": "id", "hidden": True}] for field in AddressField.fields(): address_inputs.append({"type": "text", "name": field[0], "label": field[1], "help": field[2]}) - address_repeat = { + address_repeat: dict[str, Any] = { "title": "Address", "name": "address", "type": "repeat", @@ -929,7 +931,7 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController return user_info @expose_api - def set_information(self, trans, id, payload=None, **kwd): + def set_information(self, trans: ProvidesUserContext, id, payload=None, **kwd): """ PUT /api/users/{id}/information/inputs Save a user's email, username, addresses etc. @@ -1004,7 +1006,7 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController user.preferences["extra_user_preferences"] = json.dumps(extra_user_pref_data) # Update user addresses - address_dicts = {} + address_dicts: dict[int, dict[str, Any]] = {} address_count = 0 for item in payload: match = re.match(r"^address_(?P\d+)\|(?P\S+)", item) @@ -1041,7 +1043,7 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController return {"message": "User information has been saved."} @expose_api - def get_password(self, trans, id, payload=None, **kwd): + def get_password(self, trans: ProvidesAppContext, id, payload=None, **kwd): """ Return available password inputs. """ @@ -1055,7 +1057,7 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController } @expose_api - def set_password(self, trans, id, payload=None, **kwd): + def set_password(self, trans: ProvidesAppContext, id, payload=None, **kwd): """ Allows to the logged-in user to change own password. """ @@ -1066,7 +1068,7 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController return {"message": "Password has been changed."} @expose_api - def get_permissions(self, trans, id, payload=None, **kwd): + def get_permissions(self, trans: ProvidesUserContext, id, payload=None, **kwd): """ Get the user's default permissions for the new histories """ @@ -1101,7 +1103,7 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController return {"inputs": inputs} @expose_api - def set_permissions(self, trans, id, payload=None, **kwd): + def set_permissions(self, trans: ProvidesUserContext, id, payload=None, **kwd): """ Set the user's default permissions for the new histories """ @@ -1109,13 +1111,15 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController user = self._get_user(trans, id) permissions = {} for index, action in Dataset.permitted_actions.items(): - action_id = trans.app.security_agent.get_action(action.action).action + security_action = trans.app.security_agent.get_action(action.action) + assert security_action is not None + action_id = security_action.action permissions[action_id] = [trans.sa_session.get(Role, x) for x in (payload.get(index) or [])] trans.app.security_agent.user_set_default_permissions(user, permissions) return {"message": "Permissions have been saved."} @expose_api - def get_toolbox_filters(self, trans, id, payload=None, **kwd): + def get_toolbox_filters(self, trans: ProvidesUserContext, id, payload=None, **kwd): """ API call for fetching toolbox filters data. Toolbox filters are specified in galaxy.ini. The user can activate them and the choice is stored in user_preferences. @@ -1134,14 +1138,14 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController "label": "In this section you may enable or disable Toolbox filters. Please contact your admin to configure filters as necessary.", } ] - errors = {} + errors: dict[str, str] = {} factory = FilterFactory(trans.app.toolbox) for filter_type in filter_types: self._add_filter_inputs(factory, filter_types, inputs, errors, filter_type, saved_values) return {"inputs": inputs, "errors": errors} @expose_api - def set_toolbox_filters(self, trans, id, payload=None, **kwd): + def set_toolbox_filters(self, trans: ProvidesUserContext, id, payload=None, **kwd): """ API call to update toolbox filters data. """ @@ -1205,14 +1209,14 @@ class UserAPIController(BaseGalaxyAPIController, UsesTagsMixin, BaseUIController } ) - def _get_filter_types(self, trans): + def _get_filter_types(self, trans: ProvidesAppContext): return { "toolbox_tool_filters": {"title": "Tools", "config": trans.app.config.user_tool_filters}, "toolbox_section_filters": {"title": "Sections", "config": trans.app.config.user_tool_section_filters}, "toolbox_label_filters": {"title": "Labels", "config": trans.app.config.user_tool_label_filters}, } - def _get_user(self, trans, id): + def _get_user(self, trans: ProvidesUserContext, id): user = self.get_user(trans, id) if not user: raise exceptions.RequestParameterInvalidException("Invalid user id specified.") diff --git a/lib/galaxy/webapps/galaxy/api/wes.py b/lib/galaxy/webapps/galaxy/api/wes.py index 925a40c708f..c9c03f536ee 100644 --- a/lib/galaxy/webapps/galaxy/api/wes.py +++ b/lib/galaxy/webapps/galaxy/api/wes.py @@ -69,7 +69,7 @@ class WesApi: @router.post("/ga4gh/wes/v1/runs") def submit_run( self, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, workflow_params: str | None = Form(None), workflow_type: str = Form(...), workflow_type_version: str = Form(...), diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index e4e7b233f74..bb005cd9910 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -128,6 +128,7 @@ from galaxy.webapps.galaxy.services.invocations import ( WriteInvocationStoreToPayload, ) from galaxy.webapps.galaxy.services.workflows import WorkflowsService +from galaxy.work.context import SessionRequestContext from galaxy.workflow.extract import extract_workflow from galaxy.workflow.modules import module_factory @@ -574,7 +575,7 @@ class WorkflowsAPIController( return step_dict @expose_api - def get_tool_predictions(self, trans: ProvidesUserContext, payload, **kwd): + def get_tool_predictions(self, trans: ProvidesHistoryContext, payload, **kwd): """ POST /api/workflows/get_tool_predictions Fetch predicted tools for a workflow @@ -690,7 +691,7 @@ class WorkflowsAPIController( item["url"] = url_for("workflow", id=encoded_id) return item - def _workflow_from_dict(self, trans, data, workflow_create_options, source=None): + def _workflow_from_dict(self, trans: ProvidesHistoryContext, data, workflow_create_options, source=None): """Creates a workflow from a dict. Created workflow is stored in the database and returned. @@ -715,7 +716,7 @@ class WorkflowsAPIController( self._import_tools_if_needed(trans, workflow_create_options, raw_workflow_description) return created_workflow.stored_workflow, created_workflow.missing_tools - def _import_tools_if_needed(self, trans, workflow_create_options, raw_workflow_description): + def _import_tools_if_needed(self, trans: ProvidesUserContext, workflow_create_options, raw_workflow_description): if not workflow_create_options.import_tools: return @@ -754,11 +755,11 @@ class WorkflowsAPIController( changeset_revision = item["changeset_revision"] irm.install(tool_shed_url, name, owner, changeset_revision, install_options) - def __get_stored_accessible_workflow(self, trans, workflow_id, **kwd): + def __get_stored_accessible_workflow(self, trans: ProvidesUserContext, workflow_id, **kwd): instance = util.string_as_bool(kwd.get("instance", "false")) return self.workflow_manager.get_stored_accessible_workflow(trans, workflow_id, by_stored_id=not instance) - def __get_stored_workflow(self, trans, workflow_id, **kwd): + def __get_stored_workflow(self, trans: ProvidesUserContext, workflow_id, **kwd): instance = util.string_as_bool(kwd.get("instance", "false")) return self.workflow_manager.get_stored_workflow(trans, workflow_id, by_stored_id=not instance) @@ -1012,7 +1013,7 @@ class FastAPIWorkflows: workflow_id: StoredWorkflowIDPathParam, payload: RefactorWorkflowBody, instance: InstanceQueryParam = False, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> RefactorResponse: return self.service.refactor(trans, workflow_id, payload, instance or False) @@ -1204,7 +1205,7 @@ class FastAPIWorkflows: @router.post("/api/workflow_landings/{uuid}/claim") def claim_landing( self, - trans: ProvidesUserContext = DependsOnTrans, + trans: ProvidesHistoryContext = DependsOnTrans, uuid: UUID4 = LandingUuidPathParam, payload: ClaimLandingPayload | None = Body(...), user: model.User = DependsOnUser, @@ -1214,7 +1215,7 @@ class FastAPIWorkflows: @router.get("/api/workflow_landings/{uuid}") def get_landing( self, - trans: ProvidesUserContext = DependsOnTrans, + trans: ProvidesHistoryContext = DependsOnTrans, uuid: UUID4 = LandingUuidPathParam, user: model.User = DependsOnUser, ) -> WorkflowLandingRequest: @@ -1371,7 +1372,7 @@ class FastAPIInvocations: view: SerializationViewQueryParam = None, step_details: StepDetailQueryParam = False, include_nested_invocations: bool = True, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> list[WorkflowInvocationResponse]: if not trans.user: # Anon users don't have accessible invocations (currently, though published invocations should be a thing) @@ -1424,7 +1425,7 @@ class FastAPIInvocations: instance: InvocationsInstanceQueryParam = False, view: SerializationViewQueryParam = None, step_details: StepDetailQueryParam = False, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, ) -> list[WorkflowInvocationResponse]: invocations = self.index_invocations( response=response, @@ -1451,7 +1452,7 @@ class FastAPIInvocations: def prepare_store_download( self, invocation_id: InvocationIDPathParam, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, payload: PrepareStoreDownloadPayload = Body(...), ) -> AsyncFile: return self.invocations_service.prepare_store_download( @@ -1467,7 +1468,7 @@ class FastAPIInvocations: def write_store( self, invocation_id: InvocationIDPathParam, - trans: ProvidesUserContext = DependsOnTrans, + trans: SessionRequestContext = DependsOnTrans, payload: WriteInvocationStoreToPayload = Body(...), ) -> AsyncTaskResultSummary: rval = self.invocations_service.write_store( @@ -1696,7 +1697,7 @@ class FastAPIInvocations: self, invocation_id: InvocationIDPathParam, step_id: WorkflowInvocationStepIDPathParam, - trans: ProvidesUserContext = DependsOnTrans, + trans: ProvidesHistoryContext = DependsOnTrans, payload: InvocationUpdatePayload = Body(...), ) -> InvocationStep: return self.invocations_service.update_invocation_step(trans=trans, step_id=step_id, action=payload.action) diff --git a/lib/galaxy/webapps/galaxy/controllers/admin.py b/lib/galaxy/webapps/galaxy/controllers/admin.py index e06a303c314..e2abcd691d0 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin.py @@ -34,13 +34,14 @@ from galaxy.web.framework.helpers import ( time_ago, ) from galaxy.webapps.base import controller +from galaxy.webapps.base.webapp import GalaxyWebTransaction log = logging.getLogger(__name__) class UserListGrid(grids.GridData): class StatusColumn(grids.GridColumn): - def get_value(self, trans, grid, user): + def get_value(self, trans: GalaxyWebTransaction, grid, user): if user.purged: return "Purged" elif user.deleted: @@ -48,24 +49,24 @@ class UserListGrid(grids.GridData): return "Available" class GroupsColumn(grids.GridColumn): - def get_value(self, trans, grid, user): + def get_value(self, trans: GalaxyWebTransaction, grid, user): if user.groups: return len(user.groups) return 0 class RolesColumn(grids.GridColumn): - def get_value(self, trans, grid, user): + def get_value(self, trans: GalaxyWebTransaction, grid, user): if user.roles: return len(user.roles) return 0 class LastLoginColumn(grids.GridColumn): - def get_value(self, trans, grid, user): + def get_value(self, trans: GalaxyWebTransaction, grid, user): if user.galaxy_sessions: return self.format(user.current_galaxy_session.update_time) return "never" - def sort(self, trans, query, ascending, column_name=None): + def sort(self, trans: GalaxyWebTransaction, query, ascending, column_name=None): last_login_subquery = ( trans.sa_session.query( model.GalaxySession.table.c.user_id, @@ -83,10 +84,10 @@ class UserListGrid(grids.GridData): return query class DiskUsageColumn(grids.GridColumn): - def get_value(self, trans, grid, user): + def get_value(self, trans: GalaxyWebTransaction, grid, user): return user.get_disk_usage(nice_size=True) - def sort(self, trans, query, ascending, column_name=None): + def sort(self, trans: GalaxyWebTransaction, query, ascending, column_name=None): if column_name is None: column_name = self.key column = self.model_class.table.c.get(column_name) @@ -160,13 +161,13 @@ class UserListGrid(grids.GridData): class RoleListGrid(grids.GridData): class GroupsColumn(grids.GridColumn): - def get_value(self, trans, grid, role): + def get_value(self, trans: GalaxyWebTransaction, grid, role): if role.groups: return len(role.groups) return 0 class UsersColumn(grids.GridColumn): - def get_value(self, trans, grid, role): + def get_value(self, trans: GalaxyWebTransaction, grid, role): if role.users: return len(role.users) return 0 @@ -224,13 +225,13 @@ class RoleListGrid(grids.GridData): class GroupListGrid(grids.GridData): class RolesColumn(grids.GridColumn): - def get_value(self, trans, grid, group): + def get_value(self, trans: GalaxyWebTransaction, grid, group): if group.roles: return len(group.roles) return 0 class UsersColumn(grids.GridColumn): - def get_value(self, trans, grid, group): + def get_value(self, trans: GalaxyWebTransaction, grid, group): if group.users: return len(group.users) return 0 @@ -280,23 +281,23 @@ class GroupListGrid(grids.GridData): class QuotaListGrid(grids.GridData): class AmountColumn(grids.GridColumn): - def get_value(self, trans, grid, quota): + def get_value(self, trans: GalaxyWebTransaction, grid, quota): return quota.operation + quota.display_amount class DefaultTypeColumn(grids.GridColumn): - def get_value(self, trans, grid, quota): + def get_value(self, trans: GalaxyWebTransaction, grid, quota): if quota.default: return quota.default[0].type return None class UsersColumn(grids.GridColumn): - def get_value(self, trans, grid, quota): + def get_value(self, trans: GalaxyWebTransaction, grid, quota): if quota.users: return len(quota.users) return 0 class GroupsColumn(grids.GridColumn): - def get_value(self, trans, grid, quota): + def get_value(self, trans: GalaxyWebTransaction, grid, quota): if quota.groups: return len(quota.groups) return 0 @@ -372,7 +373,7 @@ class AdminGalaxy(controller.BaseUIController): @web.expose @web.json @web.require_admin - def data_tables_list(self, trans, **kwd): + def data_tables_list(self, trans: GalaxyWebTransaction, **kwd): data = [] message = kwd.get("message", "") status = kwd.get("status", "done") @@ -395,7 +396,7 @@ class AdminGalaxy(controller.BaseUIController): @web.expose @web.json @web.require_admin - def data_types_list(self, trans, **kwd) -> DatatypesEntryT: + def data_types_list(self, trans: GalaxyWebTransaction, **kwd) -> DatatypesEntryT: datatypes = [] keys: set[str] = set() message = kwd.get("message", "") @@ -409,17 +410,17 @@ class AdminGalaxy(controller.BaseUIController): @web.expose @web.json @web.require_admin - def users_list(self, trans, **kwd): + def users_list(self, trans: GalaxyWebTransaction, **kwd): return self.user_list_grid(trans, **kwd) @web.legacy_expose_api @web.require_admin - def quotas_list(self, trans, payload=None, **kwargs): + def quotas_list(self, trans: GalaxyWebTransaction, payload=None, **kwargs): return self.quota_list_grid(trans, **kwargs) @web.legacy_expose_api @web.require_admin - def create_quota(self, trans, payload=None, **kwd): + def create_quota(self, trans: GalaxyWebTransaction, payload=None, **kwd): if trans.request.method == "GET": all_users = [] all_groups = [] @@ -441,7 +442,7 @@ class AdminGalaxy(controller.BaseUIController): default_options = [("No", "no")] for type_ in trans.app.model.DefaultQuotaAssociation.types: default_options.append((f"Yes, {type_}", type_)) - rval = { + rval: dict = { "title": "Create Quota", "inputs": [ {"name": "name", "label": "Name"}, @@ -487,7 +488,7 @@ class AdminGalaxy(controller.BaseUIController): @web.legacy_expose_api @web.require_admin - def rename_quota(self, trans, payload=None, **kwd): + def rename_quota(self, trans: GalaxyWebTransaction, payload=None, **kwd): id = kwd.get("id") if not id: return self.message_exception(trans, "No quota id received for renaming.") @@ -508,7 +509,7 @@ class AdminGalaxy(controller.BaseUIController): @web.legacy_expose_api @web.require_admin - def manage_users_and_groups_for_quota(self, trans, payload=None, **kwd): + def manage_users_and_groups_for_quota(self, trans: GalaxyWebTransaction, payload=None, **kwd): quota_id = kwd.get("id") if not quota_id: return self.message_exception(trans, f"Invalid quota id ({str(quota_id)}) received") @@ -555,7 +556,7 @@ class AdminGalaxy(controller.BaseUIController): @web.legacy_expose_api @web.require_admin - def edit_quota(self, trans, payload=None, **kwd): + def edit_quota(self, trans: GalaxyWebTransaction, payload=None, **kwd): id = kwd.get("id") if not id: return self.message_exception(trans, "No quota id received for renaming.") @@ -586,7 +587,7 @@ class AdminGalaxy(controller.BaseUIController): @web.legacy_expose_api @web.require_admin - def set_quota_default(self, trans, payload=None, **kwd): + def set_quota_default(self, trans: GalaxyWebTransaction, payload=None, **kwd): id = kwd.get("id") if not id: return self.message_exception(trans, "No quota id received for renaming.") @@ -616,7 +617,7 @@ class AdminGalaxy(controller.BaseUIController): @web.expose @web.require_admin - def impersonate(self, trans, **kwd): + def impersonate(self, trans: GalaxyWebTransaction, **kwd): if not trans.app.config.allow_user_impersonation: return trans.show_error_message("User impersonation is not enabled in this instance of Galaxy.") user = None @@ -639,12 +640,12 @@ class AdminGalaxy(controller.BaseUIController): @web.expose @web.json @web.require_admin - def roles_list(self, trans, **kwargs): + def roles_list(self, trans: GalaxyWebTransaction, **kwargs): return self.role_list_grid(trans, **kwargs) @web.legacy_expose_api @web.require_admin - def rename_role(self, trans, payload=None, **kwd): + def rename_role(self, trans: GalaxyWebTransaction, payload=None, **kwd): id = kwd.get("id") if not id: return self.message_exception(trans, "No role id received for renaming.") @@ -679,7 +680,7 @@ class AdminGalaxy(controller.BaseUIController): @web.legacy_expose_api @web.require_admin - def manage_users_and_groups_for_role(self, trans, payload=None, **kwd): + def manage_users_and_groups_for_role(self, trans: GalaxyWebTransaction, payload=None, **kwd): role_id = kwd.get("id") if not role_id: return self.message_exception(trans, f"Invalid role id ({str(role_id)}) received") @@ -729,12 +730,12 @@ class AdminGalaxy(controller.BaseUIController): @web.legacy_expose_api @web.require_admin - def groups_list(self, trans, **kwargs): + def groups_list(self, trans: GalaxyWebTransaction, **kwargs): return self.group_list_grid(trans, **kwargs) @web.legacy_expose_api @web.require_admin - def rename_group(self, trans, payload=None, **kwd): + def rename_group(self, trans: GalaxyWebTransaction, payload=None, **kwd): id = kwd.get("id") if not id: return self.message_exception(trans, "No group id received for renaming.") @@ -764,12 +765,12 @@ class AdminGalaxy(controller.BaseUIController): @web.expose @web.require_admin - def create_new_user(self, trans, **kwd): + def create_new_user(self, trans: GalaxyWebTransaction, **kwd): return trans.response.send_redirect(web.url_for(controller="user", action="create", cntrller="admin")) @web.legacy_expose_api @web.require_admin - def reset_user_password(self, trans, payload=None, **kwd): + def reset_user_password(self, trans: GalaxyWebTransaction, payload=None, **kwd): users = {user_id: get_user(trans, user_id) for user_id in util.listify(kwd.get("id"))} if users: if trans.request.method == "GET": @@ -797,7 +798,7 @@ class AdminGalaxy(controller.BaseUIController): @web.legacy_expose_api @web.require_admin - def manage_roles_and_groups_for_user(self, trans, payload=None, **kwd): + def manage_roles_and_groups_for_user(self, trans: GalaxyWebTransaction, payload=None, **kwd): user_id = kwd.get("id") if not user_id: return self.message_exception(trans, f"Invalid user id ({str(user_id)}) received") @@ -867,7 +868,7 @@ def build_select_input(name, label, options, value): } -def get_user(trans, user_id): +def get_user(trans: GalaxyWebTransaction, user_id): """Get a User from the database by id.""" user = trans.sa_session.query(trans.model.User).get(trans.security.decode_id(user_id)) if not user: @@ -875,7 +876,7 @@ def get_user(trans, user_id): return user -def get_role(trans, id): +def get_role(trans: GalaxyWebTransaction, id): """Get a Role from the database by id.""" # Load user from database id = trans.security.decode_id(id) @@ -885,7 +886,7 @@ def get_role(trans, id): return role -def get_group(trans, id): +def get_group(trans: GalaxyWebTransaction, id): """Get a Group from the database by id.""" # Load user from database id = trans.security.decode_id(id) @@ -895,7 +896,7 @@ def get_group(trans, id): return group -def get_quota(trans, id): +def get_quota(trans: GalaxyWebTransaction, id): """Get a Quota from the database by id.""" # Load user from database id = trans.security.decode_id(id) diff --git a/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py b/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py index c3ad846d83c..48d4e5b0fb7 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin_toolshed.py @@ -8,6 +8,7 @@ from galaxy import ( web, ) from galaxy.exceptions import ConfigDoesNotAllowException +from galaxy.structured_app import StructuredApp from galaxy.tool_shed.util import dependency_display from galaxy.tool_shed.util.repository_util import ( get_absolute_path_to_file_in_repository, @@ -16,6 +17,7 @@ from galaxy.tool_shed.util.repository_util import ( ) from galaxy.util import unicodify from galaxy.util.tool_shed import common_util +from galaxy.webapps.base.webapp import GalaxyWebTransaction from .admin import AdminGalaxy log = logging.getLogger(__name__) @@ -24,7 +26,7 @@ log = logging.getLogger(__name__) def legacy_tool_shed_endpoint(func): # admin only and only available if running test cases. @wraps(func) - def wrapper(trans, *args, **kwargs): + def wrapper(trans: GalaxyWebTransaction, *args, **kwargs): if not trans.app.config.config_dict.get("running_functional_tests", False): raise ConfigDoesNotAllowException("Legacy tool shed endpoint only available during testing.") return func(trans, *args, **kwargs) @@ -33,14 +35,16 @@ def legacy_tool_shed_endpoint(func): class AdminToolshed(AdminGalaxy): + app: StructuredApp + @web.json @web.require_admin @legacy_tool_shed_endpoint - def activate_repository(self, trans, **kwd): + def activate_repository(self, trans: GalaxyWebTransaction, **kwd): """Activate a repository that was deactivated but not uninstalled.""" return self._activate_repository(trans, **kwd) - def _activate_repository(self, trans, **kwd): + def _activate_repository(self, trans: GalaxyWebTransaction, **kwd): repository_id = kwd["id"] repository = get_installed_tool_shed_repository(trans.app, repository_id) try: @@ -53,7 +57,7 @@ class AdminToolshed(AdminGalaxy): @web.expose @web.require_admin @legacy_tool_shed_endpoint - def restore_repository(self, trans, **kwd): + def restore_repository(self, trans: GalaxyWebTransaction, **kwd): repository_id = kwd["id"] repository = get_installed_tool_shed_repository(trans.app, repository_id) if repository.uninstalled: @@ -62,7 +66,7 @@ class AdminToolshed(AdminGalaxy): return self._activate_repository(trans, **kwd) @web.expose - def display_image_in_repository(self, trans, **kwd): + def display_image_in_repository(self, trans: GalaxyWebTransaction, **kwd): """ Open an image file that is contained in an installed tool shed repository or that is referenced by a URL for display. The image can be defined in either a README.rst file contained in the repository or the help section of a Galaxy tool config that @@ -96,7 +100,7 @@ class AdminToolshed(AdminGalaxy): return None def _get_updated_repository_information( - self, trans, repository_id, repository_name, repository_owner, changeset_revision + self, trans: GalaxyWebTransaction, repository_id, repository_name, repository_owner, changeset_revision ): """ Send a request to the appropriate tool shed to retrieve the dictionary of information required to reinstall @@ -120,10 +124,10 @@ class AdminToolshed(AdminGalaxy): @web.json @web.require_admin @legacy_tool_shed_endpoint - def manage_repository_json(self, trans, **kwd): + def manage_repository_json(self, trans: GalaxyWebTransaction, **kwd): return self._manage_repository_json(trans, **kwd) - def _manage_repository_json(self, trans, **kwd): + def _manage_repository_json(self, trans: GalaxyWebTransaction, **kwd): repository_id = kwd.get("id", None) if repository_id is None: return trans.show_error_message("Missing required encoded repository id.") diff --git a/lib/galaxy/webapps/galaxy/controllers/async.py b/lib/galaxy/webapps/galaxy/controllers/async.py index ef9898661f6..f9fd80ed6c5 100644 --- a/lib/galaxy/webapps/galaxy/controllers/async.py +++ b/lib/galaxy/webapps/galaxy/controllers/async.py @@ -19,18 +19,19 @@ from galaxy.util import ( from galaxy.util.hash_util import hmac_new from galaxy.web import url_for from galaxy.webapps.base.controller import BaseUIController +from galaxy.webapps.base.webapp import GalaxyWebTransaction log = logging.getLogger(__name__) class ASync(BaseUIController): @web.expose - def default(self, trans, tool_id=None, data_id=None, data_secret=None, **kwd): + def default(self, trans: GalaxyWebTransaction, tool_id=None, data_id=None, data_secret=None, **kwd): """Catches the tool id and redirects as needed""" return self.index(trans, tool_id=tool_id, data_id=data_id, data_secret=data_secret, **kwd) @web.expose - def index(self, trans, tool_id=None, data_secret=None, **kwd): + def index(self, trans: GalaxyWebTransaction, tool_id=None, data_secret=None, **kwd): """Manages ascynchronous connections""" if tool_id is None: @@ -91,20 +92,18 @@ class ASync(BaseUIController): for param in params: if param in tool_declared_params or not tool.wants_params_cleaned: params_dict[param] = params.get(param, None) - params = params_dict - - if not params.get("URL"): + if not params_dict.get("URL"): return f"No URL parameter was submitted for data {data_id}" - STATUS = params.get("STATUS") + STATUS = params_dict.get("STATUS") if STATUS == "OK": key = hmac_new(trans.app.config.tool_secret, f"{data.id}:{data.history_id}") if key != data_secret: return f"You do not have permission to alter data {data_id}." - if not params.get("GALAXY_URL"): + if not params_dict.get("GALAXY_URL"): # provide a fallback for GALAXY_URL - params["GALAXY_URL"] = f"{trans.request.url_path}/async/{tool_id}/{data.id}/{key}" + params_dict["GALAXY_URL"] = f"{trans.request.url_path}/async/{tool_id}/{data.id}/{key}" # push the job into the queue data.state = data.blurb = data.states.RUNNING log.debug(f"executing tool {tool.id}") @@ -115,7 +114,7 @@ class ASync(BaseUIController): for key, obj in tool.outputs.items(): try: TOOL_OUTPUT_TYPE = obj.format - params[key] = data.id + params_dict[key] = data.id break except Exception: # exclude outputs different from ToolOutput (e.g. collections) from the previous assumption @@ -124,7 +123,7 @@ class ASync(BaseUIController): raise Exception("Error: ToolOutput object not found") original_history = trans.sa_session.query(History).get(data.history_id) - job, *_ = tool.execute(trans, incoming=params, history=original_history) + job, *_ = tool.execute(trans, incoming=params_dict, history=original_history) trans.app.job_manager.enqueue(job, tool=tool) else: log.debug(f"async error -> {STATUS}") @@ -202,6 +201,8 @@ class ASync(BaseUIController): trans.sa_session.add(trans.history) trans.sa_session.commit() # Need to explicitly create the file + assert data.dataset is not None + assert data.dataset.object_store is not None data.dataset.object_store.create(data.dataset) trans.log_event(f"Added dataset {data.id} to history {trans.history.id}", tool_id=tool_id) diff --git a/lib/galaxy/webapps/galaxy/controllers/authnz.py b/lib/galaxy/webapps/galaxy/controllers/authnz.py index 7f08ad6314d..7e631260fdf 100644 --- a/lib/galaxy/webapps/galaxy/controllers/authnz.py +++ b/lib/galaxy/webapps/galaxy/controllers/authnz.py @@ -30,7 +30,7 @@ class OIDC(BaseUIController): @web.json @web.expose @web.require_login("list third-party identities") - def index(self, trans, **kwargs): + def index(self, trans: "GalaxyWebTransaction", **kwargs): """ GET /authnz/ returns a list of third-party identities associated with the user. @@ -81,7 +81,7 @@ class OIDC(BaseUIController): @web.json @web.expose - def login(self, trans, provider, idphint=None, next=None, redirect=None): + def login(self, trans: "GalaxyWebTransaction", provider, idphint=None, next=None, redirect=None): if not trans.app.config.enable_oidc: msg = "Login to Galaxy using third-party identities is not enabled on this Galaxy instance." log.debug(msg) @@ -101,7 +101,7 @@ class OIDC(BaseUIController): raise exceptions.AuthenticationFailed(message) @web.expose - def callback(self, trans, provider, idphint=None, **kwargs): + def callback(self, trans: "GalaxyWebTransaction", provider, idphint=None, **kwargs): user = trans.user.username if trans.user is not None else "anonymous" login_next_cookie = trans.get_cookie(name=LOGIN_NEXT_COOKIE_NAME) if login_next_cookie and login_next_cookie != "None": @@ -197,7 +197,7 @@ class OIDC(BaseUIController): @web.expose @web.require_login("authenticate against the selected identity provider") - def disconnect(self, trans, provider, email=None, **kwargs): + def disconnect(self, trans: "GalaxyWebTransaction", provider, email=None, **kwargs): if trans.user is None: # Only logged in users are allowed here. return @@ -212,7 +212,7 @@ class OIDC(BaseUIController): @web.json @web.expose - def logout(self, trans, provider, **kwargs): + def logout(self, trans: "GalaxyWebTransaction", provider, **kwargs): post_user_logout_href = trans.app.config.post_user_logout_href if post_user_logout_href is not None: post_user_logout_href = trans.request.base + url_for(post_user_logout_href) @@ -226,14 +226,14 @@ class OIDC(BaseUIController): return {"message": message} @web.expose - def get_logout_url(self, trans, provider=None, **kwargs): + def get_logout_url(self, trans: "GalaxyWebTransaction", provider=None, **kwargs): idp_provider = provider if provider else trans.get_cookie(name=PROVIDER_COOKIE_NAME) if idp_provider: return trans.response.send_redirect(url_for(controller="authnz", action="logout", provider=idp_provider)) @web.expose @web.json - def get_cilogon_idps(self, trans, **kwargs): + def get_cilogon_idps(self, trans: "GalaxyWebTransaction", **kwargs): try: cilogon_idps = json.loads(url_get("https://cilogon.org/idplist/", params=dict(kwargs))) except Exception as e: diff --git a/lib/galaxy/webapps/galaxy/controllers/data_manager.py b/lib/galaxy/webapps/galaxy/controllers/data_manager.py index 14a827d7f3f..b68afc2177f 100644 --- a/lib/galaxy/webapps/galaxy/controllers/data_manager.py +++ b/lib/galaxy/webapps/galaxy/controllers/data_manager.py @@ -14,6 +14,7 @@ from galaxy.util import ( unicodify, ) from galaxy.webapps.base.controller import BaseUIController +from galaxy.webapps.base.webapp import GalaxyWebTransaction log = logging.getLogger(__name__) @@ -21,7 +22,7 @@ log = logging.getLogger(__name__) class DataManager(BaseUIController): @web.expose @web.json - def data_managers_list(self, trans, **kwd): + def data_managers_list(self, trans: GalaxyWebTransaction, **kwd): not_is_admin = not trans.user_is_admin if not_is_admin and not trans.app.config.enable_data_manager_user_view: raise paste.httpexceptions.HTTPUnauthorized( @@ -56,7 +57,7 @@ class DataManager(BaseUIController): @web.expose @web.json - def jobs_list(self, trans, **kwd): + def jobs_list(self, trans: GalaxyWebTransaction, **kwd): not_is_admin = not trans.user_is_admin if not_is_admin and not trans.app.config.enable_data_manager_user_view: raise paste.httpexceptions.HTTPUnauthorized( @@ -101,7 +102,7 @@ class DataManager(BaseUIController): @web.expose @web.json - def job_info(self, trans, **kwd): + def job_info(self, trans: GalaxyWebTransaction, **kwd): not_is_admin = not trans.user_is_admin if not_is_admin and not trans.app.config.enable_data_manager_user_view: raise paste.httpexceptions.HTTPUnauthorized( diff --git a/lib/galaxy/webapps/galaxy/controllers/dataset.py b/lib/galaxy/webapps/galaxy/controllers/dataset.py index 7b92b428119..22b3e6628c9 100644 --- a/lib/galaxy/webapps/galaxy/controllers/dataset.py +++ b/lib/galaxy/webapps/galaxy/controllers/dataset.py @@ -1,5 +1,6 @@ import logging import os +from typing import IO from urllib.parse import ( quote_plus, unquote_plus, @@ -45,6 +46,7 @@ from galaxy.webapps.base.controller import ( BaseUIController, UsesExtendedMetadataMixin, ) +from galaxy.webapps.base.webapp import GalaxyWebTransaction from galaxy.webapps.galaxy.services.datasets import DatasetsService from ..api import depends @@ -61,6 +63,7 @@ except ImportError: class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesExtendedMetadataMixin): + app: "StructuredApp" history_manager: HistoryManager = depends(HistoryManager) hda_manager: HDAManager = depends(HDAManager) hda_deserializer: HDADeserializer = depends(HDADeserializer) @@ -69,7 +72,9 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE def __init__(self, app: StructuredApp): super().__init__(app) - def _can_access_dataset(self, trans, dataset_association, allow_admin=True, additional_roles=None): + def _can_access_dataset( + self, trans: GalaxyWebTransaction, dataset_association, allow_admin=True, additional_roles=None + ): roles = trans.get_current_user_roles() if additional_roles: roles = roles + additional_roles @@ -78,11 +83,11 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE ) @web.expose - def default(self, trans, dataset_id=None, **kwd): + def default(self, trans: GalaxyWebTransaction, dataset_id=None, **kwd): return "This link may not be followed from within Galaxy." @web.expose_api_raw_anonymous_and_sessionless - def get_metadata_file(self, trans, hda_id=None, metadata_name=None, **kwd): + def get_metadata_file(self, trans: GalaxyWebTransaction, hda_id=None, metadata_name=None, **kwd): """Allows the downloading of metadata files associated with datasets (eg. bai index for bam files)""" if hda_id is None or metadata_name is None: raise RequestParameterInvalidException("Required parameters 'hda_id' and 'metadata_name' are missing.") @@ -93,7 +98,7 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE trans.response.headers.update(headers) return fh - def _check_dataset(self, trans, hda_id): + def _check_dataset(self, trans: GalaxyWebTransaction, hda_id): # DEPRECATION: We still support unencoded ids for backward compatibility try: data = trans.sa_session.query(HistoryDatasetAssociation).get(self.decode_id(hda_id)) @@ -113,7 +118,15 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE @web.expose def display( - self, trans, dataset_id=None, preview=False, filename=None, to_ext=None, offset=None, ck_size=None, **kwd + self, + trans: GalaxyWebTransaction, + dataset_id=None, + preview=False, + filename=None, + to_ext=None, + offset=None, + ck_size=None, + **kwd, ): data = self._check_dataset(trans, dataset_id) if "hdca" in kwd: @@ -140,7 +153,7 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE return display_data @web.expose_api_anonymous - def get_edit(self, trans, dataset_id=None, **kwd): + def get_edit(self, trans: GalaxyWebTransaction, dataset_id=None, **kwd): """Produces the input definitions available to modify dataset attributes""" status = None data, message = self._get_dataset_for_edit(trans, dataset_id) @@ -323,7 +336,7 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE ) @web.expose_api_anonymous - def set_edit(self, trans, payload=None, **kwd): + def set_edit(self, trans: GalaxyWebTransaction, payload=None, **kwd): """Allows user to modify parameters of an HDA.""" status = "success" operation = payload.get("operation") @@ -413,7 +426,7 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE raise MessageException(f"Invalid operation identifier ({operation}).") return {"status": status, "message": sanitize_text(message)} - def _get_dataset_for_edit(self, trans, dataset_id): + def _get_dataset_for_edit(self, trans: GalaxyWebTransaction, dataset_id): if dataset_id is not None: id = self.decode_id(dataset_id) data = trans.sa_session.get(HistoryDatasetAssociation, id) @@ -438,7 +451,7 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE return data, None @web.expose - def display_at(self, trans, dataset_id, filename=None, **kwd): + def display_at(self, trans: GalaxyWebTransaction, dataset_id, filename=None, **kwd): """Sets up a dataset permissions so it is viewable at an external site""" if not trans.app.config.enable_old_display_applications: return trans.show_error_message( @@ -472,7 +485,7 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE @web.do_not_cache def display_application( self, - trans, + trans: GalaxyWebTransaction, dataset_id=None, user_id=None, app_name=None, @@ -571,7 +584,7 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE else: file_name = value.get_file_name() content_length = os.path.getsize(file_name) - rval = open(file_name, "rb") + rval: str | IO[bytes] = open(file_name, "rb") except OSError as e: log.debug("Unable to access requested file in display application: %s", e) return paste.httpexceptions.HTTPNotFound("This file is no longer available.") diff --git a/lib/galaxy/webapps/galaxy/controllers/forms.py b/lib/galaxy/webapps/galaxy/controllers/forms.py index 58da7a21319..9d8f7f8f847 100644 --- a/lib/galaxy/webapps/galaxy/controllers/forms.py +++ b/lib/galaxy/webapps/galaxy/controllers/forms.py @@ -24,6 +24,7 @@ from galaxy.webapps.base.controller import ( BaseUIController, web, ) +from galaxy.webapps.base.webapp import GalaxyWebTransaction log = logging.getLogger(__name__) @@ -33,15 +34,15 @@ VALID_FIELDNAME_RE = re.compile(r"^[a-zA-Z0-9\_]+$") class FormsGrid(grids.GridData): # Custom column types class NameColumn(grids.GridColumn): - def get_value(self, trans, grid, form): + def get_value(self, trans: GalaxyWebTransaction, grid, form): return form.latest_form.name class DescriptionColumn(grids.GridColumn): - def get_value(self, trans, grid, form): + def get_value(self, trans: GalaxyWebTransaction, grid, form): return form.latest_form.desc class TypeColumn(grids.GridColumn): - def get_value(self, trans, grid, form): + def get_value(self, trans: GalaxyWebTransaction, grid, form): return form.latest_form.type # Grid definition @@ -100,12 +101,12 @@ class Forms(BaseUIController): @web.legacy_expose_api @web.require_admin - def forms_list(self, trans, payload=None, **kwd): + def forms_list(self, trans: GalaxyWebTransaction, payload=None, **kwd): return self.forms_grid(trans, **kwd) @web.legacy_expose_api @web.require_admin - def create_form(self, trans, payload=None, **kwd): + def create_form(self, trans: GalaxyWebTransaction, payload=None, **kwd): if trans.request.method == "GET": fd_types = sorted(model.FormDefinition.types.__members__.items()) return { @@ -157,7 +158,7 @@ class Forms(BaseUIController): @web.legacy_expose_api @web.require_admin - def edit_form(self, trans, payload=None, **kwd): + def edit_form(self, trans: GalaxyWebTransaction, payload=None, **kwd): id = kwd.get("id") if not id: return self.message_exception(trans, "No form id received for editing.") @@ -166,8 +167,8 @@ class Forms(BaseUIController): if trans.request.method == "GET": fd_types = sorted(model.FormDefinition.types.__members__.items()) ff_types = [(t.__name__, t.__name__) for t in model.FormDefinition.supported_field_types] - field_cache = [] - field_inputs = [ + field_cache: list = [] + field_inputs: list[dict] = [ { "name": "name", "label": "Name", @@ -223,7 +224,7 @@ class Forms(BaseUIController): message = f"The form '{payload.get('name')}' has been updated." return {"message": message} - def get_current_form(self, trans, payload=None, **kwd): + def get_current_form(self, trans: GalaxyWebTransaction, payload=None, **kwd): """ This method gets all the unsaved user-entered form details and returns a dictionary containing the name, desc, type, layout & fields of the form @@ -249,7 +250,7 @@ class Forms(BaseUIController): break return dict(name=name, desc=desc, type=type, layout=[], fields=fields) - def save_form_definition(self, trans, form_id=None, payload=None, **kwd): + def save_form_definition(self, trans: GalaxyWebTransaction, form_id=None, payload=None, **kwd): """ This method saves a form given an id """ diff --git a/lib/galaxy/webapps/galaxy/controllers/history.py b/lib/galaxy/webapps/galaxy/controllers/history.py index 4d438a49a94..0257fd1fd65 100644 --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -33,6 +33,7 @@ from galaxy.webapps.base.controller import ( BaseUIController, SharableMixin, ) +from galaxy.webapps.base.webapp import GalaxyWebTransaction from galaxy.webapps.galaxy.services.histories import HistoriesService from ..api import depends @@ -49,11 +50,19 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt super().__init__(app) @web.expose - def index(self, trans): + def index(self, trans: GalaxyWebTransaction): return "" @expose_api_anonymous - def view(self, trans, id=None, show_deleted=False, show_hidden=False, use_panels=True, **kwargs): + def view( + self, + trans: GalaxyWebTransaction, + id=None, + show_deleted=False, + show_hidden=False, + use_panels=True, + **kwargs, + ): """ View a history. If a history is importable, then it is viewable by any user. """ @@ -93,7 +102,7 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt "allow_user_dataset_purge": trans.app.config.allow_user_dataset_purge, } - def _display_by_username_and_slug(self, trans, username, slug, **kwargs): + def _display_by_username_and_slug(self, trans: GalaxyWebTransaction, username, slug, **kwargs): """ Display history based on a username and slug. """ @@ -129,7 +138,7 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt @web.expose_api @web.require_login("changing default permissions") - def permissions(self, trans, payload=None, **kwd): + def permissions(self, trans: GalaxyWebTransaction, payload=None, **kwd): """ Sets the permissions on a history. """ @@ -146,7 +155,7 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt for action_key, action in Dataset.permitted_actions.items(): in_roles = set() for a in current_actions: - if a.action == action.action: + if a.action == action.action and a.role is not None: in_roles.add(a.role) role_tuples = [] @@ -172,15 +181,15 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt self.history_manager.error_unless_mutable(history) permissions = {} for action_key, action in Dataset.permitted_actions.items(): - in_roles = payload.get(action_key) or [] - in_roles = [trans.sa_session.get(Role, trans.security.decode_id(x)) for x in in_roles] - permissions[trans.app.security_agent.get_action(action.action)] = in_roles + role_ids_in = payload.get(action_key) or [] + selected_roles = [trans.sa_session.get(Role, trans.security.decode_id(x)) for x in role_ids_in] + permissions[trans.app.security_agent.get_action(action.action)] = selected_roles trans.app.security_agent.history_set_default_permissions(history, permissions) return {"message": f"Default history '{history.name}' dataset permissions have been changed."} @web.expose_api @web.require_login("make datasets private") - def make_private(self, trans, history_id=None, all_histories=False, **kwd): + def make_private(self, trans: GalaxyWebTransaction, history_id=None, all_histories=False, **kwd): """ Sets the datasets within a history to private. Also sets the default permissions for the history to private, for future datasets. @@ -226,7 +235,7 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt # ......................................................................... actions/orig. async @web.expose - def purge_deleted_datasets(self, trans): + def purge_deleted_datasets(self, trans: GalaxyWebTransaction): count = 0 if trans.app.config.allow_user_dataset_purge and trans.history: for hda in trans.history.datasets: @@ -251,7 +260,7 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt return trans.show_error_message("Cannot purge deleted datasets from this session.") @web.expose_api_anonymous - def resume_paused_jobs(self, trans, current=False, ids=None, **kwargs): + def resume_paused_jobs(self, trans: GalaxyWebTransaction, current=False, ids=None, **kwargs): """Resume paused jobs for the active history -- this does not require a logged in user.""" if not ids and string_as_bool(current): history = trans.get_history() @@ -264,7 +273,7 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt @web.expose_api @web.require_login("rename histories") - def rename(self, trans, payload=None, **kwd): + def rename(self, trans: GalaxyWebTransaction, payload=None, **kwd): id = kwd.get("id") if not id: raise exceptions.RequestParameterMissingException("No history id received for renaming.") @@ -309,17 +318,17 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt # ------------------------------------------------------------------------- current history @web.expose @web.require_login("switch to a history") - def switch_to_history(self, trans, hist_id=None, **kwargs): + def switch_to_history(self, trans: GalaxyWebTransaction, hist_id=None, **kwargs): """Change the current user's current history to one with `hist_id`.""" # remains for backwards compat self.set_as_current(trans, id=hist_id) return trans.response.send_redirect(url_for("/")) - def get_item(self, trans, id): + def get_item(self, trans: GalaxyWebTransaction, id): return self.history_manager.get_owned(self.decode_id(id), trans.user, current_history=trans.history) # TODO: override of base ui controller? - def history_data(self, trans, history): + def history_data(self, trans: GalaxyWebTransaction, history): """Return the given history in a serialized, dictionary form.""" return self.history_serializer.serialize_to_view(history, view="dev-detailed", user=trans.user, trans=trans) @@ -327,7 +336,7 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt # @web.require_login( "switch to a history" ) @web.json @web.do_not_cache - def set_as_current(self, trans, id, **kwargs): + def set_as_current(self, trans: GalaxyWebTransaction, id, **kwargs): """Change the current user's current history to one with `id`.""" try: history = self.history_manager.get_owned(self.decode_id(id), trans.user, current_history=trans.history) @@ -339,7 +348,7 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt @web.json @web.do_not_cache - def current_history_json(self, trans, since=None, **kwargs): + def current_history_json(self, trans: GalaxyWebTransaction, since=None, **kwargs): """Return the current user's current history in a serialized, dictionary form.""" history = trans.get_history(most_recent=True, create=True) if since: @@ -356,7 +365,7 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt return self.history_data(trans, history) @web.json - def create_new_current(self, trans, name=None, **kwargs): + def create_new_current(self, trans: GalaxyWebTransaction, name=None, **kwargs): """Create a new, current history for the current user""" new_history = trans.new_history(name) return self.history_data(trans, new_history) diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index 625270346a8..31bda110c9a 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -7,17 +7,20 @@ from galaxy.webapps.base.controller import ( SharableItemSecurityMixin, SharableMixin, ) +from galaxy.webapps.base.webapp import GalaxyWebTransaction class PageController(BaseUIController, SharableMixin, SharableItemSecurityMixin): def __init__(self, app: StructuredApp): super().__init__(app) - def _display_by_username_and_slug(self, trans, username, slug, **kwargs): + def _display_by_username_and_slug(self, trans: GalaxyWebTransaction, username, slug, **kwargs): """Display page based on a username and slug.""" # Get page. user = get_user_by_username(trans.sa_session, username) + if user is None: + raise web.httpexceptions.HTTPNotFound() page = get_page(trans.sa_session, user, slug) if page is None: raise web.httpexceptions.HTTPNotFound() diff --git a/lib/galaxy/webapps/galaxy/controllers/root.py b/lib/galaxy/webapps/galaxy/controllers/root.py index bad03966b2f..817dcdf1580 100644 --- a/lib/galaxy/webapps/galaxy/controllers/root.py +++ b/lib/galaxy/webapps/galaxy/controllers/root.py @@ -34,7 +34,7 @@ class RootController(controller.BaseUIController, UsesAnnotations): super().__init__(app) @web.expose - def default(self, trans, target1=None, target2=None, **kwd): + def default(self, trans: GalaxyWebTransaction, target1=None, target2=None, **kwd): """ Called on any url that does not match a controller method. """ diff --git a/lib/galaxy/webapps/galaxy/controllers/shed_tool_static.py b/lib/galaxy/webapps/galaxy/controllers/shed_tool_static.py index 6e88939b2ee..f1fd8bfe675 100644 --- a/lib/galaxy/webapps/galaxy/controllers/shed_tool_static.py +++ b/lib/galaxy/webapps/galaxy/controllers/shed_tool_static.py @@ -11,6 +11,7 @@ from galaxy.util.path import ( safe_contains, ) from galaxy.webapps.base.controller import BaseUIController +from galaxy.webapps.base.webapp import GalaxyWebTransaction log = logging.getLogger(__name__) @@ -23,7 +24,7 @@ def _asset_exists_and_is_safe(repo_path, asset_path): class ShedToolStatic(BaseUIController): @web.expose - def index(self, trans, shed, owner, repo, tool, version, image_file, **kwargs): + def index(self, trans: GalaxyWebTransaction, shed, owner, repo, tool, version, image_file, **kwargs): """ Open an image file that is contained in an installed tool shed repository or that is referenced by a URL for display. The image can be defined in either a README.rst file contained in the repository or the help section of a Galaxy tool config that diff --git a/lib/galaxy/webapps/galaxy/controllers/tool_runner.py b/lib/galaxy/webapps/galaxy/controllers/tool_runner.py index c5112e63390..5680f9c4c1b 100644 --- a/lib/galaxy/webapps/galaxy/controllers/tool_runner.py +++ b/lib/galaxy/webapps/galaxy/controllers/tool_runner.py @@ -17,6 +17,7 @@ from galaxy.web import ( url_for, ) from galaxy.webapps.base.controller import BaseUIController +from galaxy.webapps.base.webapp import GalaxyWebTransaction log = logging.getLogger(__name__) @@ -24,18 +25,18 @@ log = logging.getLogger(__name__) class ToolRunner(BaseUIController): # Hack to get biomart to work, ideally, we could pass tool_id to biomart and receive it back @web.expose - def biomart(self, trans, tool_id="biomart", **kwd): + def biomart(self, trans: GalaxyWebTransaction, tool_id="biomart", **kwd): """Catches the tool id and redirects as needed""" return self.index(trans, tool_id=tool_id, **kwd) # test to get hapmap to work, ideally, we could pass tool_id to hapmap biomart and receive it back @web.expose - def hapmapmart(self, trans, tool_id="hapmapmart", **kwd): + def hapmapmart(self, trans: GalaxyWebTransaction, tool_id="hapmapmart", **kwd): """Catches the tool id and redirects as needed""" return self.index(trans, tool_id=tool_id, **kwd) @web.expose - def default(self, trans, tool_id=None, **kwd): + def default(self, trans: GalaxyWebTransaction, tool_id=None, **kwd): """Catches the tool id and redirects as needed""" return self.index(trans, tool_id=tool_id, **kwd) @@ -53,7 +54,7 @@ class ToolRunner(BaseUIController): return self.get_toolbox().get_tool(tool_id, tool_version=tool_version) @web.expose - def index(self, trans, tool_id=None, from_noframe=None, **kwd): + def index(self, trans: GalaxyWebTransaction, tool_id=None, from_noframe=None, **kwd): def __tool_404__(): log.debug("index called with tool id '%s' but no such tool exists", tool_id) trans.log_event(f"Tool id '{tool_id}' does not exist") @@ -131,7 +132,7 @@ class ToolRunner(BaseUIController): return trans.response.send_redirect(url_for("/?notification=tool-submitted")) @web.expose - def rerun(self, trans, id=None, job_id=None, **kwd): + def rerun(self, trans: GalaxyWebTransaction, id=None, job_id=None, **kwd): """ Given a HistoryDatasetAssociation id, find the job and that created the dataset, extract the parameters, and display the appropriate tool @@ -165,7 +166,7 @@ class ToolRunner(BaseUIController): return trans.response.send_redirect(url_for(f"/?job_id={job_id}")) @web.expose - def data_source_redirect(self, trans, tool_id=None): + def data_source_redirect(self, trans: GalaxyWebTransaction, tool_id=None): """ Redirects a user accessing a Data Source tool to its target action link. This method will subvert mix-mode content blocking in several browsers when diff --git a/lib/galaxy/webapps/galaxy/controllers/user.py b/lib/galaxy/webapps/galaxy/controllers/user.py index 60f5fe7ef4f..2abd78069df 100644 --- a/lib/galaxy/webapps/galaxy/controllers/user.py +++ b/lib/galaxy/webapps/galaxy/controllers/user.py @@ -34,6 +34,7 @@ from galaxy.webapps.base.controller import ( BaseUIController, UsesFormDefinitionsMixin, ) +from galaxy.webapps.base.webapp import GalaxyWebTransaction from ..api import depends log = logging.getLogger(__name__) @@ -52,7 +53,7 @@ class User(BaseUIController, UsesFormDefinitionsMixin): def __handle_role_and_group_auto_creation( self, - trans, + trans: GalaxyWebTransaction, user, roles, auto_create_roles=False, @@ -99,7 +100,7 @@ class User(BaseUIController, UsesFormDefinitionsMixin): trans.log_event("Assigning role to newly created user") trans.app.security_agent.associate_user_role(user, role) - def __autoregistration(self, trans, login, password): + def __autoregistration(self, trans: GalaxyWebTransaction, login, password): """ Does the autoregistration if enabled. Returns a message """ @@ -140,11 +141,11 @@ class User(BaseUIController, UsesFormDefinitionsMixin): return message, user @expose_api_anonymous_and_sessionless - def login(self, trans, payload=None, **kwd): + def login(self, trans: GalaxyWebTransaction, payload=None, **kwd): payload = payload or {} return self.__validate_login(trans, payload, **kwd) - def __validate_login(self, trans, payload=None, **kwd): + def __validate_login(self, trans: GalaxyWebTransaction, payload=None, **kwd): """Handle Galaxy Log in""" if not payload: payload = kwd @@ -211,7 +212,7 @@ class User(BaseUIController, UsesFormDefinitionsMixin): return {"message": "Success.", "redirect": self.__get_redirect_url(redirect)} @web.expose - def resend_verification(self, trans, **kwargs): + def resend_verification(self, trans: GalaxyWebTransaction, **kwargs): """ Exposed function for use outside of the class. E.g. when user click on the resend link in the masthead. """ @@ -221,7 +222,7 @@ class User(BaseUIController, UsesFormDefinitionsMixin): else: return trans.show_error_message(message) - def resend_activation_email(self, trans, email, username): + def resend_activation_email(self, trans: GalaxyWebTransaction, email, username): """ Function resends the verification email in case user wants to log in with an inactive account or he clicks the resend link. """ @@ -240,7 +241,7 @@ class User(BaseUIController, UsesFormDefinitionsMixin): message += f" Error contact: {trans.app.config.error_email_to}." return message, is_activation_sent - def is_outside_grace_period(self, trans, create_time): + def is_outside_grace_period(self, trans: GalaxyWebTransaction, create_time): """ Function checks whether the user is outside the config-defined grace period for inactive accounts. """ @@ -252,7 +253,7 @@ class User(BaseUIController, UsesFormDefinitionsMixin): @web.expose @web.json - def logout(self, trans, logout_all=False, **kwd): + def logout(self, trans: GalaxyWebTransaction, logout_all=False, **kwd): if message := trans.check_csrf_token(kwd): return self.message_exception(trans, message) # Since logging an event requires a session, we'll log prior to ending the session @@ -264,7 +265,7 @@ class User(BaseUIController, UsesFormDefinitionsMixin): return success_response @expose_api_anonymous_and_sessionless - def create(self, trans, payload=None, **kwd): + def create(self, trans: GalaxyWebTransaction, payload=None, **kwd): if not payload: payload = kwd message = trans.check_csrf_token(payload) @@ -280,7 +281,7 @@ class User(BaseUIController, UsesFormDefinitionsMixin): return {"message": "Success."} @web.expose - def activate(self, trans, **kwd): + def activate(self, trans: GalaxyWebTransaction, **kwd): """ Check whether token fits the user and then activate the user's account. """ @@ -332,7 +333,7 @@ class User(BaseUIController, UsesFormDefinitionsMixin): return trans.show_error_message(invalid_link_message) @expose_api_anonymous_and_sessionless - def change_password(self, trans, payload=None, **kwd): + def change_password(self, trans: GalaxyWebTransaction, payload=None, **kwd): """ Allows to change own password. @@ -352,7 +353,7 @@ class User(BaseUIController, UsesFormDefinitionsMixin): return {"message": "Password has been changed."} @expose_api_anonymous_and_sessionless - def reset_password(self, trans, payload=None, **kwd): + def reset_password(self, trans: GalaxyWebTransaction, payload=None, **kwd): """Reset the user's password. Send an email with token that allows a password change.""" payload = payload or {} if message := self.user_manager.send_reset_email(trans, payload): diff --git a/lib/galaxy/webapps/galaxy/controllers/visualization.py b/lib/galaxy/webapps/galaxy/controllers/visualization.py index 9da5323480e..c38c6513708 100644 --- a/lib/galaxy/webapps/galaxy/controllers/visualization.py +++ b/lib/galaxy/webapps/galaxy/controllers/visualization.py @@ -20,6 +20,7 @@ from galaxy.webapps.base.controller import ( SharableMixin, UsesVisualizationMixin, ) +from galaxy.webapps.base.webapp import GalaxyWebTransaction from ..api import depends log = logging.getLogger(__name__) @@ -28,13 +29,16 @@ log = logging.getLogger(__name__) class VisualizationController( BaseUIController, SharableMixin, UsesVisualizationMixin, UsesAnnotations, UsesItemRatings ): + app: "StructuredApp" hda_manager: HDAManager = depends(HDAManager) slug_builder: SlugBuilder = depends(SlugBuilder) def __init__(self, app: StructuredApp): super().__init__(app) - def get_visualization(self, trans, visualization_id, check_ownership=True, check_accessible=False): + def get_visualization( + self, trans: GalaxyWebTransaction, visualization_id, check_ownership=True, check_accessible=False + ): """ Get a Visualization from the database by id, verifying ownership. """ @@ -50,7 +54,7 @@ class VisualizationController( @web.expose @web.require_login() - def copy(self, trans, id, **kwargs): + def copy(self, trans: GalaxyWebTransaction, id, **kwargs): visualization = self.get_visualization(trans, id, check_ownership=False, check_accessible=True) user = trans.get_user() owner = visualization.user == user @@ -71,7 +75,7 @@ class VisualizationController( @web.expose @web.require_login("share Galaxy visualizations") - def imp(self, trans, id, **kwargs): + def imp(self, trans: GalaxyWebTransaction, id, **kwargs): """Import a visualization into user's workspace.""" # Set referer message. referer = trans.request.referer @@ -112,7 +116,7 @@ class VisualizationController( use_panels=True, ) - def _display_by_username_and_slug(self, trans, username, slug, **kwargs): + def _display_by_username_and_slug(self, trans: GalaxyWebTransaction, username, slug, **kwargs): """Display visualization based on a username and slug.""" # Get visualization. @@ -141,7 +145,7 @@ class VisualizationController( @web.legacy_expose_api @web.require_login("edit visualizations") - def edit(self, trans, payload=None, **kwd): + def edit(self, trans: GalaxyWebTransaction, payload=None, **kwd): """ Edit a visualization's attributes. """ diff --git a/lib/galaxy/webapps/galaxy/controllers/workflow.py b/lib/galaxy/webapps/galaxy/controllers/workflow.py index 45bfe0ecb01..d6419945a2c 100644 --- a/lib/galaxy/webapps/galaxy/controllers/workflow.py +++ b/lib/galaxy/webapps/galaxy/controllers/workflow.py @@ -18,6 +18,7 @@ from galaxy.webapps.base.controller import ( SharableMixin, UsesStoredWorkflowMixin, ) +from galaxy.webapps.base.webapp import GalaxyWebTransaction from ..api import depends log = logging.getLogger(__name__) @@ -31,7 +32,7 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi def __init__(self, app: StructuredApp): super().__init__(app) - def _display_by_username_and_slug(self, trans, username, slug, format="html", **kwargs): + def _display_by_username_and_slug(self, trans: GalaxyWebTransaction, username, slug, format="html", **kwargs): """ Display workflow based on a username and slug. Format can be html, json, or json-download. """ @@ -56,7 +57,7 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi elif format == "json-download": return self.export_to_file(trans, encoded_id) - def _display(self, trans, stored_workflow): + def _display(self, trans: GalaxyWebTransaction, stored_workflow): """Diplay workflow in client.""" if stored_workflow is None: raise web.httpexceptions.HTTPNotFound() @@ -88,7 +89,7 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi @web.expose @web.require_login("to import a workflow", use_panels=True) - def imp(self, trans, id, **kwargs): + def imp(self, trans: GalaxyWebTransaction, id, **kwargs): """Imports a workflow shared by other users.""" # Set referer message. referer = trans.request.referer @@ -120,7 +121,7 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi @web.expose @web.require_login("use Galaxy workflows") - def gen_image(self, trans, id, embed="false", version="", **kwargs): + def gen_image(self, trans: GalaxyWebTransaction, id, embed="false", version="", **kwargs): embed = util.asbool(embed) if version: version_int_or_none = int(version) @@ -138,7 +139,7 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi return trans.show_error_message(error_message) @web.json_pretty - def for_direct_import(self, trans, id, **kwargs): + def for_direct_import(self, trans: GalaxyWebTransaction, id, **kwargs): """ Get the latest Workflow for the StoredWorkflow identified by `id` and encode it as a json string that can be imported back into Galaxy @@ -151,7 +152,7 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi return self._workflow_to_dict(trans, stored) @web.json_pretty - def export_to_file(self, trans, id, **kwds): + def export_to_file(self, trans: GalaxyWebTransaction, id, **kwds): """ Get the latest Workflow for the StoredWorkflow identified by `id` and export it to a JSON file that can be imported back into Galaxy. diff --git a/lib/galaxy/webapps/galaxy/services/_fetch_util.py b/lib/galaxy/webapps/galaxy/services/_fetch_util.py index 4632dfc4e42..686d7aa8a7f 100644 --- a/lib/galaxy/webapps/galaxy/services/_fetch_util.py +++ b/lib/galaxy/webapps/galaxy/services/_fetch_util.py @@ -7,6 +7,7 @@ from galaxy.actions.library import ( ) from galaxy.exceptions import RequestParameterInvalidException from galaxy.files.uris import validate_non_local +from galaxy.managers.context import ProvidesUserContext from galaxy.model.store.discover import ( get_required_item, replace_request_syntax_sugar, @@ -22,7 +23,7 @@ VALID_DESTINATION_TYPES = ["library", "library_folder", "hdca", "hdas"] ELEMENTS_FROM_TRANSIENT_TYPES = ["archive", "bagit_archive"] -def validate_and_normalize_targets(trans, payload, set_internal_fields=True): +def validate_and_normalize_targets(trans: ProvidesUserContext, payload, set_internal_fields=True): """Validate and normalize all src references in fetch targets. - Normalize ftp_import and server_dir src entries into simple path entries @@ -106,6 +107,10 @@ def validate_and_normalize_targets(trans, payload, set_internal_fields=True): # It'd be nice if this can be de-duplicated with what is in parameters/grouping.py. user_ftp_dir = trans.user_ftp_dir + if user_ftp_dir is None: + raise RequestParameterInvalidException( + "FTP import not permitted or FTP upload directory not configured" + ) is_directory = False assert not os.path.islink(user_ftp_dir), "User FTP directory cannot be a symbolic link" diff --git a/lib/galaxy/webapps/galaxy/services/base.py b/lib/galaxy/webapps/galaxy/services/base.py index ac735b18280..025211e035f 100644 --- a/lib/galaxy/webapps/galaxy/services/base.py +++ b/lib/galaxy/webapps/galaxy/services/base.py @@ -120,7 +120,9 @@ class ServiceBase: """ return get_class(class_name) - def get_object(self, trans, id, class_name, check_ownership=False, check_accessible=False, deleted=None): + def get_object( + self, trans: ProvidesUserContext, id, class_name, check_ownership=False, check_accessible=False, deleted=None + ): """ Convenience method to get a model object with the specified checks. """ @@ -167,7 +169,7 @@ class ServesExportStores: class ConsumesModelStores: def create_objects_from_store( self, - trans, + trans: ProvidesUserContext, payload, history=None, for_library=False, diff --git a/lib/galaxy/webapps/galaxy/services/datasets.py b/lib/galaxy/webapps/galaxy/services/datasets.py index 41ee441eafd..23596f01f4c 100644 --- a/lib/galaxy/webapps/galaxy/services/datasets.py +++ b/lib/galaxy/webapps/galaxy/services/datasets.py @@ -27,7 +27,11 @@ from galaxy.celery.tasks import compute_dataset_hash from galaxy.datatypes.binary import Binary from galaxy.datatypes.dataproviders.exceptions import NoProviderAvailable from galaxy.managers.base import ModelSerializer -from galaxy.managers.context import ProvidesHistoryContext +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.managers.datasets import ( DatasetAssociationManager, DatasetManager, @@ -915,12 +919,14 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin): raise galaxy_exceptions.InternalServerError(f"Could not get content for dataset: {util.unicodify(e)}") return content, headers - def update_object_store_id(self, trans, dataset_id: DecodedDatabaseIdField, payload: UpdateObjectStoreIdPayload): + def update_object_store_id( + self, trans: ProvidesUserContext, dataset_id: DecodedDatabaseIdField, payload: UpdateObjectStoreIdPayload + ): hda = self.hda_manager.get_accessible(dataset_id, trans.user) dataset = hda.dataset self.dataset_manager.update_object_store_id(trans, dataset, payload.object_store_id) - def _get_or_create_converted(self, trans, original: model.DatasetInstance, target_ext: str): + def _get_or_create_converted(self, trans: ProvidesUserContext, original: model.DatasetInstance, target_ext: str): try: original.get_converted_dataset(trans, target_ext) converted = original.get_converted_files_by_type(target_ext) @@ -950,7 +956,7 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin): def _converted_datasets_state( self, - trans, + trans: ProvidesUserContext, dataset: model.DatasetInstance, chrom: str | None = None, retry: bool = False, @@ -988,7 +994,7 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin): def _search_features( self, - trans, + trans: ProvidesUserContext, dataset: model.DatasetInstance, query: str | None, ) -> list[list[str]]: @@ -1116,7 +1122,7 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin): def _raw_data( self, - trans, + trans: ProvidesAppContext, dataset, provider=None, **kwargs, @@ -1155,7 +1161,7 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin): return data - def _get_indexer(self, trans, dataset): + def _get_indexer(self, trans: ProvidesAppContext, dataset): indexer = self.data_provider_registry.get_data_provider(trans, original_dataset=dataset, source="index") if indexer is None: msg = f"No indexer available for dataset {self.encode_id(dataset.id)}" diff --git a/lib/galaxy/webapps/galaxy/services/histories.py b/lib/galaxy/webapps/galaxy/services/histories.py index a6fc25aa511..8e604b4d9a0 100644 --- a/lib/galaxy/webapps/galaxy/services/histories.py +++ b/lib/galaxy/webapps/galaxy/services/histories.py @@ -31,8 +31,12 @@ from galaxy.celery.tasks import ( ) from galaxy.files.uris import validate_uri_access from galaxy.managers.citations import CitationsManager -from galaxy.managers.context import ProvidesHistoryContext +from galaxy.managers.context import ( + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.managers.histories import ( + CurrentHistoryContext, HistoryDeserializer, HistoryExportManager, HistoryFilters, @@ -99,6 +103,7 @@ from galaxy.schema.workflows import ( from galaxy.security.idencoding import IdEncodingHelper from galaxy.short_term_storage import ShortTermStorageAllocator from galaxy.util import restore_text +from galaxy.webapps.base.webapp import GalaxyWebTransaction from galaxy.webapps.galaxy.services.base import ( ConsumesModelStores, model_store_storage_target, @@ -121,7 +126,9 @@ DEFAULT_ORDER_BY = "create_time-dsc" class ShareableHistoryService(ShareableService): share_with_status_cls = ShareHistoryWithStatus - def share_with_users(self, trans, id: DecodedDatabaseIdField, payload: ShareWithPayload) -> ShareHistoryWithStatus: + def share_with_users( + self, trans: ProvidesUserContext, id: DecodedDatabaseIdField, payload: ShareWithPayload + ) -> ShareHistoryWithStatus: return cast(ShareHistoryWithStatus, super().share_with_users(trans, id, payload)) @@ -160,7 +167,7 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores): def index( self, - trans: ProvidesHistoryContext, + trans: CurrentHistoryContext, serialization_params: SerializationParams, filter_query_params: FilterQueryParams, deleted_only: bool | None = False, @@ -238,7 +245,7 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores): def index_query( self, - trans, + trans: ProvidesHistoryContext, payload: HistoryIndexQueryPayload, serialization_params: SerializationParams, include_total_count: bool = False, @@ -256,7 +263,7 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores): def create( self, - trans: ProvidesHistoryContext, + trans: CurrentHistoryContext, payload: CreateHistoryPayload, serialization_params: SerializationParams, ): @@ -336,7 +343,7 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores): def create_from_store( self, - trans, + trans: ProvidesHistoryContext, payload: CreateHistoryFromStore, serialization_params: SerializationParams, ) -> AnyHistoryView: @@ -349,7 +356,7 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores): def create_from_store_async( self, - trans, + trans: ProvidesUserContext, payload: CreateHistoryFromStore, ) -> AsyncTaskResultSummary: self._ensure_can_create_history(trans) @@ -363,7 +370,7 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores): result = import_model_store.delay(request=request, task_user_id=getattr(trans.user, "id", None)) return async_task_summary(result) - def _ensure_can_create_history(self, trans): + def _ensure_can_create_history(self, trans: ProvidesUserContext): if trans.anonymous: raise glx_exceptions.AuthenticationRequired("You need to be logged in to create histories.") if trans.user and trans.user.bootstrap_admin_user: @@ -587,7 +594,7 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores): def count( self, - trans: ProvidesHistoryContext, + trans: CurrentHistoryContext, ): """ Returns number of histories for the current user. @@ -660,7 +667,7 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores): def archive_export( self, - trans, + trans: ProvidesHistoryContext, history_id: DecodedDatabaseIdField, payload: ExportHistoryArchivePayload | None = None, ) -> tuple[HistoryArchiveExportResult, bool]: @@ -745,7 +752,7 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores): # removing the legacy HistoriesController def legacy_archive_download( self, - trans: ProvidesHistoryContext, + trans: GalaxyWebTransaction, history_id: DecodedDatabaseIdField, jeha_id: DecodedDatabaseIdField, ): diff --git a/lib/galaxy/webapps/galaxy/services/history_contents.py b/lib/galaxy/webapps/galaxy/services/history_contents.py index c438dd268d2..f7d19d79ba6 100644 --- a/lib/galaxy/webapps/galaxy/services/history_contents.py +++ b/lib/galaxy/webapps/galaxy/services/history_contents.py @@ -43,6 +43,7 @@ from galaxy.managers.collections_util import ( dictify_dataset_collection_instance, ) from galaxy.managers.context import ( + ProvidesAppContext, ProvidesHistoryContext, ProvidesUserContext, ) @@ -313,7 +314,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def index( self, - trans, + trans: ProvidesHistoryContext, history_id: DecodedDatabaseIdField, params: HistoryContentsIndexParams, legacy_params: LegacyHistoryContentsIndexParams, @@ -332,7 +333,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def show( self, - trans, + trans: ProvidesHistoryContext, id: DecodedDatabaseIdField, serialization_params: SerializationParams, contents_type: HistoryContentType, @@ -433,7 +434,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def index_jobs_summary( self, - trans, + trans: ProvidesAppContext, params: HistoryContentsIndexJobsSummaryParams, ) -> list[AnyJobStateSummary]: """ @@ -454,7 +455,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def show_jobs_summary( self, - trans, + trans: ProvidesHistoryContext, id: DecodedDatabaseIdField, contents_type: HistoryContentType, ) -> AnyJobStateSummary: @@ -488,7 +489,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto assert job is None or implicit_collection_jobs is None return self.encode_all_ids(summarize_jobs_to_dict(trans.sa_session, job or implicit_collection_jobs)) - def get_dataset_collection_archive_for_download(self, trans, id: DecodedDatabaseIdField): + def get_dataset_collection_archive_for_download(self, trans: ProvidesHistoryContext, id: DecodedDatabaseIdField): """ Download the content of a HistoryDatasetCollection as a tgz archive while maintaining approximate collection structure. @@ -498,7 +499,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto dataset_collection_instance = self.__get_accessible_collection(trans, id) return self.__stream_dataset_collection(trans, dataset_collection_instance) - def prepare_collection_download(self, trans, id: DecodedDatabaseIdField) -> AsyncFile: + def prepare_collection_download(self, trans: ProvidesHistoryContext, id: DecodedDatabaseIdField) -> AsyncFile: ensure_celery_tasks_enabled(trans.app.config) dataset_collection_instance = self.__get_accessible_collection(trans, id) archive_name = f"{dataset_collection_instance.hid}: {dataset_collection_instance.name}" @@ -514,7 +515,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto ) return AsyncFile(storage_request_id=short_term_storage_target.request_id, task=async_task_summary(result)) - def __stream_dataset_collection(self, trans, dataset_collection_instance): + def __stream_dataset_collection(self, trans: ProvidesUserContext, dataset_collection_instance): archive = hdcas.stream_dataset_collection( dataset_collection_instance=dataset_collection_instance, upstream_mod_zip=trans.app.config.upstream_mod_zip, @@ -525,7 +526,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def create( self, - trans, + trans: ProvidesHistoryContext, history_id: DecodedDatabaseIdField, payload: CreateHistoryContentPayload, serialization_params: SerializationParams, @@ -567,7 +568,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def create_from_store( self, - trans, + trans: ProvidesHistoryContext, history_id: DecodedDatabaseIdField, payload: CreateHistoryContentFromStore, serialization_params: SerializationParams, @@ -595,7 +596,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def materialize( self, - trans, + trans: ProvidesHistoryContext, request: MaterializeDatasetInstanceRequest, ) -> AsyncTaskResultSummary: # DO THIS JUST TO MAKE SURE IT IS OWNED... @@ -613,7 +614,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def update_permissions( self, - trans, + trans: ProvidesHistoryContext, history_content_id: DecodedDatabaseIdField, payload: UpdateDatasetPermissionsPayload, ) -> DatasetAssociationRoles: @@ -643,7 +644,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def update( self, - trans, + trans: ProvidesHistoryContext, history_id: DecodedDatabaseIdField | None, id: DecodedDatabaseIdField, payload: dict[str, Any], @@ -679,7 +680,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def update_batch( self, - trans, + trans: ProvidesHistoryContext, history_id: DecodedDatabaseIdField, payload: UpdateHistoryContentsBatchPayload, serialization_params: SerializationParams, @@ -853,7 +854,12 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto search=search, ) - def validate(self, trans, history_id: DecodedDatabaseIdField, history_content_id: DecodedDatabaseIdField): + def validate( + self, + trans: ProvidesHistoryContext, + history_id: DecodedDatabaseIdField, + history_content_id: DecodedDatabaseIdField, + ): """ Validates the metadata associated with a dataset within a History. @@ -874,7 +880,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def delete( self, - trans, + trans: ProvidesHistoryContext, id: DecodedDatabaseIdField, contents_type: HistoryContentType, payload: DeleteHistoryContentPayload, @@ -893,7 +899,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def archive( self, - trans, + trans: ProvidesUserContext, history_id: DecodedDatabaseIdField, filter_query_params: FilterQueryParams, filename: str | None = "", @@ -1001,7 +1007,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto archive.write(file_path, archive_path) return archive - def __delete_dataset(self, trans, id: DecodedDatabaseIdField, purge: bool, stop_job: bool): + def __delete_dataset(self, trans: ProvidesHistoryContext, id: DecodedDatabaseIdField, purge: bool, stop_job: bool): hda = self.hda_manager.get_owned(id, trans.user, current_history=trans.history) assert hda.history is not None self.history_manager.error_unless_mutable(hda.history) @@ -1013,12 +1019,14 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto self.hda_manager.delete(hda, stop_job=stop_job) return None - def __update_dataset_collection(self, trans, id: DecodedDatabaseIdField, payload: dict[str, Any]): + def __update_dataset_collection( + self, trans: ProvidesHistoryContext, id: DecodedDatabaseIdField, payload: dict[str, Any] + ): return self.dataset_collection_manager.update(trans, "history", id, payload) def __update_dataset( self, - trans, + trans: ProvidesUserContext, history: History, id: DecodedDatabaseIdField, payload: dict[str, Any], @@ -1036,7 +1044,11 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto return {} def __datasets_for_update( - self, trans, history: History, hda_ids: list[DecodedDatabaseIdField], payload: dict[str, Any] + self, + trans: ProvidesUserContext, + history: History, + hda_ids: list[DecodedDatabaseIdField], + payload: dict[str, Any], ): anonymous_user = not trans.user_is_admin and trans.user is None if anonymous_user: @@ -1057,7 +1069,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto return hdas - def __deserialize_dataset(self, trans, hda, payload: dict[str, Any]): + def __deserialize_dataset(self, trans: ProvidesUserContext, hda, payload: dict[str, Any]): # TODO: when used in batch it would be a lot faster if we set flush=false # and the caller flushes only at the end or when a given chunk size is reached. self.hda_deserializer.deserialize(hda, payload, user=trans.user, trans=trans, flush=True) @@ -1067,7 +1079,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def __index_legacy( self, - trans, + trans: ProvidesHistoryContext, history_id: DecodedDatabaseIdField, legacy_params: LegacyHistoryContentsIndexParams, ) -> HistoryContentsResult: @@ -1093,7 +1105,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def __index_v2( self, - trans, + trans: ProvidesHistoryContext, history_id: DecodedDatabaseIdField, params: HistoryContentsIndexParams, serialization_params: SerializationParams, @@ -1157,7 +1169,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def _serialize_legacy_content_item( self, - trans, + trans: ProvidesUserContext, content, dataset_details: DatasetDetailsType | None = None, ): @@ -1174,7 +1186,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def _serialize_content_item( self, - trans, + trans: ProvidesUserContext, content, dataset_details: DatasetDetailsType | None, serialization_params: SerializationParams, @@ -1218,7 +1230,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto ) return rval - def __collection_dict(self, trans, dataset_collection_instance, **kwds): + def __collection_dict(self, trans: ProvidesAppContext, dataset_collection_instance, **kwds): return dictify_dataset_collection_instance( dataset_collection_instance, security=trans.security, @@ -1227,14 +1239,14 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto **kwds, ) - def _get_history(self, trans, history_id: DecodedDatabaseIdField) -> History: + def _get_history(self, trans: ProvidesHistoryContext, history_id: DecodedDatabaseIdField) -> History: """Retrieves the History with the given ID or raises an error if the current user cannot access it.""" history = self.history_manager.get_accessible(history_id, trans.user, current_history=trans.history) return history def __show_dataset( self, - trans, + trans: ProvidesUserContext, id: DecodedDatabaseIdField, serialization_params: SerializationParams, ): @@ -1246,7 +1258,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def __show_dataset_collection( self, - trans, + trans: ProvidesHistoryContext, id: DecodedDatabaseIdField, serialization_params: SerializationParams, fuzzy_count: int | None = None, @@ -1255,7 +1267,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto view = serialization_params.view or "element" return self.__collection_dict(trans, dataset_collection_instance, view=view, fuzzy_count=fuzzy_count) - def __get_accessible_collection(self, trans, id: DecodedDatabaseIdField): + def __get_accessible_collection(self, trans: ProvidesHistoryContext, id: DecodedDatabaseIdField): return self.dataset_collection_manager.get_dataset_collection_instance( trans=trans, instance_type="history", @@ -1264,7 +1276,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def __create_datasets_from_library_folder( self, - trans, + trans: ProvidesUserContext, history: History, payload: CreateHistoryContentPayloadFromCopy, serialization_params: SerializationParams, @@ -1324,7 +1336,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def __create_dataset( self, - trans, + trans: ProvidesHistoryContext, history: History, payload: CreateHistoryContentPayloadFromCopy, serialization_params: SerializationParams, @@ -1350,7 +1362,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto hda, user=trans.user, trans=trans, encode_id=False, **serialization_params.model_dump() ) - def __create_hda_from_ldda(self, trans, history: History, ldda_id: int): + def __create_hda_from_ldda(self, trans: ProvidesUserContext, history: History, ldda_id: int): decoded_ldda_id = ldda_id ld = self.ldda_manager.get(trans, decoded_ldda_id) if type(ld) is not LibraryDataset: @@ -1358,7 +1370,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto hda = ld.library_dataset_dataset_association.to_history_dataset_association(history, add_to_history=True) return hda - def __create_hda_from_copy(self, trans, history: History, original_hda_id: int): + def __create_hda_from_copy(self, trans: ProvidesHistoryContext, history: History, original_hda_id: int): original = self.hda_manager.get_accessible(original_hda_id, trans.user) assert original.history is not None # check for access on history that contains the original hda as well @@ -1368,7 +1380,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto def __create_dataset_collection( self, - trans, + trans: ProvidesHistoryContext, history: History, payload: CreateHistoryContentPayloadFromCollection, serialization_params: SerializationParams, @@ -1499,7 +1511,7 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto ) def _get_contents_by_item_list( - self, trans, history: History, items: list[HistoryContentItem] + self, trans: ProvidesHistoryContext, history: History, items: list[HistoryContentItem] ) -> list["HistoryItem"]: contents: list[HistoryItem] = [] diff --git a/lib/galaxy/webapps/galaxy/services/invocations.py b/lib/galaxy/webapps/galaxy/services/invocations.py index b71d810c9a4..3889933aba0 100644 --- a/lib/galaxy/webapps/galaxy/services/invocations.py +++ b/lib/galaxy/webapps/galaxy/services/invocations.py @@ -15,6 +15,7 @@ from galaxy.exceptions import ( ObjectNotFound, ) from galaxy.managers.context import ( + ProvidesAppContext, ProvidesHistoryContext, ProvidesUserContext, ) @@ -65,6 +66,7 @@ from galaxy.webapps.galaxy.services.base import ( model_store_storage_target, ServiceBase, ) +from galaxy.work.context import SessionRequestContext log = logging.getLogger(__name__) @@ -93,7 +95,10 @@ class InvocationsService(ServiceBase, ConsumesModelStores): self._export_tracker = export_tracker def index( - self, trans, invocation_payload: InvocationIndexPayload, serialization_params: InvocationSerializationParams + self, + trans: ProvidesHistoryContext, + invocation_payload: InvocationIndexPayload, + serialization_params: InvocationSerializationParams, ) -> tuple[list[WorkflowInvocationResponse], int]: workflow_id = invocation_payload.workflow_id if invocation_payload.instance: @@ -134,11 +139,11 @@ class InvocationsService(ServiceBase, ConsumesModelStores): invocation_dict = self.serialize_workflow_invocations(invocations, serialization_params) return invocation_dict, total_matches - def show(self, trans, invocation_id, serialization_params): + def show(self, trans: ProvidesUserContext, invocation_id, serialization_params): wfi = self._workflows_manager.get_invocation(trans, invocation_id, check_ownership=False, check_accessible=True) return self.serialize_workflow_invocation(wfi, serialization_params) - def get_invocation(self, trans, invocation_id) -> WorkflowInvocation: + def get_invocation(self, trans: ProvidesUserContext, invocation_id) -> WorkflowInvocation: """Get the raw WorkflowInvocation model object.""" return self._workflows_manager.get_invocation( trans, invocation_id, check_ownership=False, check_accessible=True @@ -148,15 +153,15 @@ class InvocationsService(ServiceBase, ConsumesModelStores): wfi = self._workflows_manager.get_invocation(trans, invocation_id, check_ownership=True, check_accessible=True) return self.serialize_workflow_invocation_to_request(trans, wfi) - def cancel(self, trans, invocation_id, serialization_params): + def cancel(self, trans: ProvidesUserContext, invocation_id, serialization_params): wfi = self._workflows_manager.request_invocation_cancellation(trans, invocation_id) return self.serialize_workflow_invocation(wfi, serialization_params) - def show_invocation_report(self, trans, invocation_id, format="json"): + def show_invocation_report(self, trans: ProvidesUserContext, invocation_id, format="json"): wfi_report = self._workflows_manager.get_invocation_report(trans, invocation_id, format=format) return wfi_report - def show_invocation_step(self, trans, step_id) -> InvocationStep: + def show_invocation_step(self, trans: ProvidesUserContext, step_id) -> InvocationStep: wfi_step = self._workflows_manager.get_invocation_step( trans, step_id, check_ownership=False, check_accessible=True ) @@ -185,11 +190,11 @@ class InvocationsService(ServiceBase, ConsumesModelStores): metrics_dict["step_label"] = step_label return metrics_dict_list - def update_invocation_step(self, trans, step_id, action) -> InvocationStep: + def update_invocation_step(self, trans: ProvidesHistoryContext, step_id, action) -> InvocationStep: wfi_step = self._workflows_manager.update_invocation_step(trans, step_id, action) return self.serialize_workflow_invocation_step(wfi_step) - def show_invocation_step_jobs_summary(self, trans, invocation_id) -> list[dict[str, Any]]: + def show_invocation_step_jobs_summary(self, trans: ProvidesAppContext, invocation_id) -> list[dict[str, Any]]: ids = [] types = [] for job_source_type, job_source_id, _ in invocation_job_source_iter(trans.sa_session, invocation_id): @@ -197,13 +202,13 @@ class InvocationsService(ServiceBase, ConsumesModelStores): types.append(job_source_type) return fetch_job_states(trans.sa_session, ids, types) - def show_invocation_jobs_summary(self, trans, invocation_id) -> dict[str, Any]: + def show_invocation_jobs_summary(self, trans: ProvidesAppContext, invocation_id) -> dict[str, Any]: ids = [invocation_id] types = ["WorkflowInvocation"] return fetch_job_states(trans.sa_session, ids, types)[0] def prepare_store_download( - self, trans, invocation_id: DecodedDatabaseIdField, payload: PrepareStoreDownloadPayload + self, trans: SessionRequestContext, invocation_id: DecodedDatabaseIdField, payload: PrepareStoreDownloadPayload ) -> AsyncFile: ensure_celery_tasks_enabled(trans.app.config) model_store_format = payload.model_store_format @@ -239,7 +244,10 @@ class InvocationsService(ServiceBase, ConsumesModelStores): return AsyncFile(storage_request_id=short_term_storage_target.request_id, task=task_summary) def write_store( - self, trans, invocation_id: DecodedDatabaseIdField, payload: WriteInvocationStoreToPayload + self, + trans: SessionRequestContext, + invocation_id: DecodedDatabaseIdField, + payload: WriteInvocationStoreToPayload, ) -> AsyncTaskResultSummary: ensure_celery_tasks_enabled(trans.app.config) workflow_invocation = self._workflows_manager.get_invocation( diff --git a/lib/galaxy/webapps/galaxy/services/jobs.py b/lib/galaxy/webapps/galaxy/services/jobs.py index b7d98a9cae9..bab92187ea4 100644 --- a/lib/galaxy/webapps/galaxy/services/jobs.py +++ b/lib/galaxy/webapps/galaxy/services/jobs.py @@ -19,6 +19,7 @@ from galaxy.celery.tasks import queue_jobs from galaxy.managers import hdas from galaxy.managers.base import security_check from galaxy.managers.context import ( + ProvidesAppContext, ProvidesHistoryContext, ProvidesUserContext, ) @@ -214,13 +215,13 @@ class JobsService(ServiceBase): # Raise an exception if neither job_id nor dataset_id is provided raise ValueError("Either job_id or dataset_id must be provided.") - def dictify_associations(self, trans, *association_lists) -> list[JobAssociation]: + def dictify_associations(self, trans: ProvidesAppContext, *association_lists) -> list[JobAssociation]: rval: list[JobAssociation] = [] for association_list in association_lists: rval.extend(self.__dictify_association(trans, a) for a in association_list) return rval - def __dictify_association(self, trans, job_dataset_association) -> JobAssociation: + def __dictify_association(self, trans: ProvidesAppContext, job_dataset_association) -> JobAssociation: dataset_dict = None if dataset := job_dataset_association.dataset: if isinstance(dataset, model.HistoryDatasetAssociation): @@ -229,7 +230,9 @@ class JobsService(ServiceBase): dataset_dict = {"src": "ldda", "id": dataset.id} return JobAssociation(name=job_dataset_association.name, dataset=dataset_dict) - def dictify_output_collection_associations(self, trans, job: model.Job) -> list[JobOutputCollectionAssociation]: + def dictify_output_collection_associations( + self, trans: ProvidesAppContext, job: model.Job + ) -> list[JobOutputCollectionAssociation]: output_associations: list[JobOutputCollectionAssociation] = [] for job_output_collection_association in job.output_dataset_collection_instances: ref_dict = {"src": "hdca", "id": job_output_collection_association.dataset_collection_id} diff --git a/lib/galaxy/webapps/galaxy/services/libraries.py b/lib/galaxy/webapps/galaxy/services/libraries.py index 0b280fefa05..5c13f3bd724 100644 --- a/lib/galaxy/webapps/galaxy/services/libraries.py +++ b/lib/galaxy/webapps/galaxy/services/libraries.py @@ -7,7 +7,10 @@ from galaxy import ( exceptions, util, ) -from galaxy.managers.context import ProvidesAppContext +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesUserContext, +) from galaxy.managers.folders import FolderManager from galaxy.managers.libraries import LibraryManager from galaxy.managers.roles import RoleManager @@ -58,7 +61,7 @@ class LibrariesService(ServiceBase, ConsumesModelStores): self.library_manager = library_manager self.role_manager = role_manager - def index(self, trans: ProvidesAppContext, deleted: bool | None = False) -> LibrarySummaryList: + def index(self, trans: ProvidesUserContext, deleted: bool | None = False) -> LibrarySummaryList: """Returns a list of summary data for all libraries. :param deleted: if True, show only ``deleted`` libraries, if False show only ``non-deleted`` @@ -77,12 +80,12 @@ class LibrariesService(ServiceBase, ConsumesModelStores): libraries.append(LibrarySummary(**library_dict)) return LibrarySummaryList(root=libraries) - def show(self, trans, id: DecodedDatabaseIdField) -> LibrarySummary: + def show(self, trans: ProvidesUserContext, id: DecodedDatabaseIdField) -> LibrarySummary: """Returns detailed information about a library.""" library = self.library_manager.get(trans, id) return self._to_summary(trans, library) - def create(self, trans, payload: CreateLibraryPayload) -> LibrarySummary: + def create(self, trans: ProvidesUserContext, payload: CreateLibraryPayload) -> LibrarySummary: """Creates a new library. .. note:: Currently, only admin users can create libraries. @@ -90,7 +93,7 @@ class LibrariesService(ServiceBase, ConsumesModelStores): library = self.library_manager.create(trans, payload.name, payload.description, payload.synopsis) return self._to_summary(trans, library) - def create_from_store(self, trans, payload: CreateLibrariesFromStore) -> list[LibrarySummary]: + def create_from_store(self, trans: ProvidesUserContext, payload: CreateLibrariesFromStore) -> list[LibrarySummary]: object_tracker = self.create_objects_from_store( trans, payload, @@ -101,7 +104,9 @@ class LibrariesService(ServiceBase, ConsumesModelStores): rval.append(self._to_summary(trans, library)) return rval - def update(self, trans, id: DecodedDatabaseIdField, payload: UpdateLibraryPayload) -> LibrarySummary: + def update( + self, trans: ProvidesUserContext, id: DecodedDatabaseIdField, payload: UpdateLibraryPayload + ) -> LibrarySummary: """Updates the library with given ``id`` with the data in the payload.""" library = self.library_manager.get(trans, id) name = payload.name @@ -112,7 +117,9 @@ class LibrariesService(ServiceBase, ConsumesModelStores): updated_library = self.library_manager.update(trans, library, name, payload.description, payload.synopsis) return self._to_summary(trans, updated_library) - def delete(self, trans, id: DecodedDatabaseIdField, undelete: bool | None = False) -> LibrarySummary: + def delete( + self, trans: ProvidesUserContext, id: DecodedDatabaseIdField, undelete: bool | None = False + ) -> LibrarySummary: """Marks the library with the given ``id`` as `deleted` (or removes the `deleted` mark if the `undelete` param is true) .. note:: Currently, only admin users can un/delete libraries. @@ -128,7 +135,7 @@ class LibrariesService(ServiceBase, ConsumesModelStores): def get_permissions( self, - trans, + trans: ProvidesUserContext, id: DecodedDatabaseIdField, scope: LibraryPermissionScope | None = LibraryPermissionScope.current, is_library_access: bool | None = False, @@ -185,7 +192,7 @@ class LibrariesService(ServiceBase, ConsumesModelStores): ) def set_permissions( - self, trans, id: DecodedDatabaseIdField, payload: dict[str, Any] + self, trans: ProvidesUserContext, id: DecodedDatabaseIdField, payload: dict[str, Any] ) -> LibraryLegacySummary | LibraryCurrentPermissions: # Old legacy response """Set permissions of the given library to the given role ids. @@ -315,7 +322,7 @@ class LibrariesService(ServiceBase, ConsumesModelStores): roles = self.library_manager.get_current_roles(trans, library) return LibraryCurrentPermissions.model_construct(**roles) - def set_permissions_old(self, trans, library, payload: dict[str, Any]) -> LibraryLegacySummary: + def set_permissions_old(self, trans: ProvidesAppContext, library, payload: dict[str, Any]) -> LibraryLegacySummary: """ *** old implementation for backward compatibility *** @@ -333,6 +340,6 @@ class LibrariesService(ServiceBase, ConsumesModelStores): item = library.to_dict(view="element") return LibraryLegacySummary(**item) - def _to_summary(self, trans, library) -> LibrarySummary: + def _to_summary(self, trans: ProvidesUserContext, library) -> LibrarySummary: library_dict = self.library_manager.get_library_dict(trans, library) return LibrarySummary(**library_dict) diff --git a/lib/galaxy/webapps/galaxy/services/library_folder_contents.py b/lib/galaxy/webapps/galaxy/services/library_folder_contents.py index 689bc7de369..b6d519dce2d 100644 --- a/lib/galaxy/webapps/galaxy/services/library_folder_contents.py +++ b/lib/galaxy/webapps/galaxy/services/library_folder_contents.py @@ -7,7 +7,10 @@ from galaxy import ( util, ) from galaxy.managers import base as managers_base -from galaxy.managers.context import ProvidesUserContext +from galaxy.managers.context import ( + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.managers.folders import FolderManager from galaxy.managers.hdas import HDAManager from galaxy.model import tags @@ -47,7 +50,9 @@ class LibraryFolderContentsService(ServiceBase, UsesLibraryMixinItems): self.hda_manager = hda_manager self.folder_manager = folder_manager - def get_object(self, trans, id, class_name, check_ownership=False, check_accessible=False, deleted=None): + def get_object( + self, trans: ProvidesUserContext, id, class_name, check_ownership=False, check_accessible=False, deleted=None + ): """ Convenience method to get a model object with the specified checks. """ @@ -87,7 +92,7 @@ class LibraryFolderContentsService(ServiceBase, UsesLibraryMixinItems): def create( self, - trans: ProvidesUserContext, + trans: ProvidesHistoryContext, folder_id: LibraryFolderDatabaseIdField, payload: CreateLibraryFilePayload, ): diff --git a/lib/galaxy/webapps/galaxy/services/library_folders.py b/lib/galaxy/webapps/galaxy/services/library_folders.py index 2c0e27814bb..8035738d8d6 100644 --- a/lib/galaxy/webapps/galaxy/services/library_folders.py +++ b/lib/galaxy/webapps/galaxy/services/library_folders.py @@ -6,6 +6,7 @@ from galaxy.exceptions import ( RequestParameterInvalidException, RequestParameterMissingException, ) +from galaxy.managers.context import ProvidesUserContext from galaxy.managers.folders import FolderManager from galaxy.managers.roles import RoleManager from galaxy.model.db.role import get_private_role_user_emails_dict @@ -36,7 +37,7 @@ class LibraryFoldersService(ServiceBase): self.folder_manager = folder_manager self.role_manager = role_manager - def show(self, trans, folder_id: LibraryFolderDatabaseIdField) -> LibraryFolderDetails: + def show(self, trans: ProvidesUserContext, folder_id: LibraryFolderDatabaseIdField) -> LibraryFolderDetails: """ Displays information about a folder. @@ -51,7 +52,10 @@ class LibraryFoldersService(ServiceBase): return LibraryFolderDetails(**return_dict) def create( - self, trans, parent_folder_id: LibraryFolderDatabaseIdField, payload: CreateLibraryFolderPayload + self, + trans: ProvidesUserContext, + parent_folder_id: LibraryFolderDatabaseIdField, + payload: CreateLibraryFolderPayload, ) -> LibraryFolderDetails: """ Create a new folder object underneath the one specified in the parameters. @@ -76,7 +80,7 @@ class LibraryFoldersService(ServiceBase): def get_permissions( self, - trans, + trans: ProvidesUserContext, folder_id: LibraryFolderDatabaseIdField, scope: LibraryPermissionScope | None = LibraryPermissionScope.current, page: int = 1, @@ -125,7 +129,7 @@ class LibraryFoldersService(ServiceBase): ) def set_permissions( - self, trans, folder_id: LibraryFolderDatabaseIdField, payload: dict + self, trans: ProvidesUserContext, folder_id: LibraryFolderDatabaseIdField, payload: dict ) -> LibraryFolderCurrentPermissions: """ Set permissions of the given folder to the given role ids. @@ -225,7 +229,7 @@ class LibraryFoldersService(ServiceBase): return LibraryFolderCurrentPermissions(**current_permissions) def delete( - self, trans, folder_id: LibraryFolderDatabaseIdField, undelete: bool | None = False + self, trans: ProvidesUserContext, folder_id: LibraryFolderDatabaseIdField, undelete: bool | None = False ) -> LibraryFolderDetails: """ Mark the folder with the given ``encoded_folder_id`` as `deleted` @@ -248,7 +252,7 @@ class LibraryFoldersService(ServiceBase): return LibraryFolderDetails(**folder_dict) def update( - self, trans, folder_id: LibraryFolderDatabaseIdField, payload: UpdateLibraryFolderPayload + self, trans: ProvidesUserContext, folder_id: LibraryFolderDatabaseIdField, payload: UpdateLibraryFolderPayload ) -> LibraryFolderDetails: """ Update the folder with id ``folder_id`` with the data in the payload. diff --git a/lib/galaxy/webapps/galaxy/services/pages.py b/lib/galaxy/webapps/galaxy/services/pages.py index beaae1b27e1..26fdc566a41 100644 --- a/lib/galaxy/webapps/galaxy/services/pages.py +++ b/lib/galaxy/webapps/galaxy/services/pages.py @@ -4,6 +4,10 @@ from galaxy import exceptions from galaxy.celery.helpers import async_task_summary from galaxy.celery.tasks import prepare_pdf_download from galaxy.managers import base +from galaxy.managers.context import ( + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.managers.markdown_util import ( internal_galaxy_markdown_to_pdf, to_basic_markdown, @@ -65,7 +69,7 @@ class PagesService(ServiceBase): self.short_term_storage_allocator = short_term_storage_allocator def index( - self, trans, payload: PageIndexQueryPayload, include_total_count: bool = False + self, trans: ProvidesUserContext, payload: PageIndexQueryPayload, include_total_count: bool = False ) -> tuple[PageSummaryList, int | None]: """Return a list of Pages viewable by the user @@ -83,7 +87,7 @@ class PagesService(ServiceBase): total_matches, ) - def _page_to_details(self, trans, page) -> PageDetails: + def _page_to_details(self, trans: ProvidesHistoryContext, page) -> PageDetails: """Serialize a Page (with the content of its latest revision) to PageDetails.""" rval = page.to_dict() rval["annotation"] = get_item_annotation_str(trans.sa_session, trans.user, page) @@ -93,14 +97,14 @@ class PagesService(ServiceBase): self.manager.rewrite_content_for_export(trans, rval) return PageDetails(**rval) - def create(self, trans, payload: CreatePagePayload) -> PageDetails: + def create(self, trans: ProvidesHistoryContext, payload: CreatePagePayload) -> PageDetails: """ Create a page and return it. """ page = self.manager.create_page(trans, payload) return self._page_to_details(trans, page) - def delete(self, trans, id: DecodedDatabaseIdField): + def delete(self, trans: ProvidesUserContext, id: DecodedDatabaseIdField): """ Mark page as deleted @@ -111,7 +115,7 @@ class PagesService(ServiceBase): page.deleted = True trans.sa_session.commit() - def undelete(self, trans, id: DecodedDatabaseIdField): + def undelete(self, trans: ProvidesUserContext, id: DecodedDatabaseIdField): """ Undelete page @@ -122,7 +126,7 @@ class PagesService(ServiceBase): page.deleted = False trans.sa_session.commit() - def show(self, trans, id: DecodedDatabaseIdField) -> PageDetails: + def show(self, trans: ProvidesHistoryContext, id: DecodedDatabaseIdField) -> PageDetails: """View a page summary and the content of the latest revision :param id: ID of page to be displayed @@ -133,7 +137,7 @@ class PagesService(ServiceBase): page = base.get_object(trans, id, "Page", check_ownership=False, check_accessible=True) return self._page_to_details(trans, page) - def show_pdf(self, trans, id: DecodedDatabaseIdField): + def show_pdf(self, trans: ProvidesHistoryContext, id: DecodedDatabaseIdField): """ View a page summary and the content of the latest revision as PDF. @@ -148,7 +152,7 @@ class PagesService(ServiceBase): internal_galaxy_markdown = page.latest_revision.content return internal_galaxy_markdown_to_pdf(trans, internal_galaxy_markdown, PdfDocumentType.page) - def prepare_pdf(self, trans, id: DecodedDatabaseIdField) -> AsyncFile: + def prepare_pdf(self, trans: ProvidesHistoryContext, id: DecodedDatabaseIdField) -> AsyncFile: ensure_celery_tasks_enabled(trans.app.config) page = base.get_object(trans, id, "Page", check_ownership=False, check_accessible=True) short_term_storage_target = self.short_term_storage_allocator.new_target( @@ -166,14 +170,16 @@ class PagesService(ServiceBase): result = prepare_pdf_download.delay(request=pdf_download_request, task_user_id=getattr(trans.user, "id", None)) return AsyncFile(storage_request_id=request_id, task=async_task_summary(result)) - def update(self, trans, id: PageIdPathParam, payload: UpdatePagePayload) -> PageDetails: + def update(self, trans: ProvidesHistoryContext, id: PageIdPathParam, payload: UpdatePagePayload) -> PageDetails: """ Update a page and return it. """ page = self.manager.update_page(trans, id, payload) return self._page_to_details(trans, page) - def list_revisions(self, trans, id: DecodedDatabaseIdField, sort_desc: bool = False) -> PageRevisionList: + def list_revisions( + self, trans: ProvidesUserContext, id: DecodedDatabaseIdField, sort_desc: bool = False + ) -> PageRevisionList: page = base.get_object(trans, id, "Page", check_ownership=False, check_accessible=True) revisions = self.manager.list_revisions(trans, page, sort_desc=sort_desc) return PageRevisionList( @@ -190,7 +196,7 @@ class PagesService(ServiceBase): ) def show_revision( - self, trans, id: DecodedDatabaseIdField, revision_id: DecodedDatabaseIdField + self, trans: ProvidesHistoryContext, id: DecodedDatabaseIdField, revision_id: DecodedDatabaseIdField ) -> PageRevisionDetails: page = base.get_object(trans, id, "Page", check_ownership=False, check_accessible=True) revision = self.manager.get_revision(trans, page, revision_id) @@ -200,7 +206,7 @@ class PagesService(ServiceBase): return PageRevisionDetails(**rval) def revert_revision( - self, trans, id: DecodedDatabaseIdField, revision_id: DecodedDatabaseIdField + self, trans: ProvidesHistoryContext, id: DecodedDatabaseIdField, revision_id: DecodedDatabaseIdField ) -> PageRevisionDetails: page = base.get_object(trans, id, "Page", check_ownership=True, check_accessible=True) new_revision = self.manager.restore_revision(trans, page, revision_id) diff --git a/lib/galaxy/webapps/galaxy/services/quotas.py b/lib/galaxy/webapps/galaxy/services/quotas.py index 29c538a7d01..7927a54860e 100644 --- a/lib/galaxy/webapps/galaxy/services/quotas.py +++ b/lib/galaxy/webapps/galaxy/services/quotas.py @@ -7,7 +7,10 @@ from sqlalchemy import ( ) from galaxy import util -from galaxy.managers.context import ProvidesUserContext +from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesUserContext, +) from galaxy.managers.groups import get_group_by_name from galaxy.managers.quotas import QuotaManager from galaxy.model import Quota @@ -121,7 +124,7 @@ class QuotasService(ServiceBase): quota = self.quota_manager.get_quota(trans, id, deleted=True) return self.quota_manager.undelete_quota(quota) - def validate_in_users_and_groups(self, trans, payload): + def validate_in_users_and_groups(self, trans: ProvidesAppContext, payload): """ For convenience, in_users and in_groups can be encoded IDs or emails/group names in the API. """ diff --git a/lib/galaxy/webapps/galaxy/services/sharable.py b/lib/galaxy/webapps/galaxy/services/sharable.py index 9a6144049d1..8ca3d2de618 100644 --- a/lib/galaxy/webapps/galaxy/services/sharable.py +++ b/lib/galaxy/webapps/galaxy/services/sharable.py @@ -1,6 +1,7 @@ import logging from galaxy.managers import base +from galaxy.managers.context import ProvidesUserContext from galaxy.managers.sharable import ( SharableModelManager, SharableModelSerializer, @@ -58,16 +59,16 @@ class ShareableService: self.serializer = serializer self.notification_service = notification_service - def set_slug(self, trans, id: DecodedDatabaseIdField, payload: SetSlugPayload): + def set_slug(self, trans: ProvidesUserContext, id: DecodedDatabaseIdField, payload: SetSlugPayload): item = self._get_item_by_id(trans, id) self.manager.set_slug(item, payload.new_slug, trans.user) - def sharing(self, trans, id: DecodedDatabaseIdField) -> SharingStatus: + def sharing(self, trans: ProvidesUserContext, id: DecodedDatabaseIdField) -> SharingStatus: """Gets the current sharing status of the item with the given id.""" item = self._get_item_by_id(trans, id) return self._get_sharing_status(trans, item) - def enable_link_access(self, trans, id: DecodedDatabaseIdField) -> SharingStatus: + def enable_link_access(self, trans: ProvidesUserContext, id: DecodedDatabaseIdField) -> SharingStatus: """Makes this item accessible by link. If this item contains other elements they will be publicly accessible too. """ @@ -76,12 +77,12 @@ class ShareableService: self.manager.make_importable(item) return self._get_sharing_status(trans, item) - def disable_link_access(self, trans, id: DecodedDatabaseIdField) -> SharingStatus: + def disable_link_access(self, trans: ProvidesUserContext, id: DecodedDatabaseIdField) -> SharingStatus: item = self._get_item_by_id(trans, id) self.manager.make_non_importable(item) return self._get_sharing_status(trans, item) - def publish(self, trans, id: DecodedDatabaseIdField) -> SharingStatus: + def publish(self, trans: ProvidesUserContext, id: DecodedDatabaseIdField) -> SharingStatus: """Makes this item publicly accessible. If this item contains other elements they will be publicly accessible too. """ @@ -90,12 +91,14 @@ class ShareableService: self.manager.publish(item) return self._get_sharing_status(trans, item) - def unpublish(self, trans, id: DecodedDatabaseIdField) -> SharingStatus: + def unpublish(self, trans: ProvidesUserContext, id: DecodedDatabaseIdField) -> SharingStatus: item = self._get_item_by_id(trans, id) self.manager.unpublish(item) return self._get_sharing_status(trans, item) - def share_with_users(self, trans, id: DecodedDatabaseIdField, payload: ShareWithPayload) -> ShareWithStatus: + def share_with_users( + self, trans: ProvidesUserContext, id: DecodedDatabaseIdField, payload: ShareWithPayload + ) -> ShareWithStatus: item = self._get_item_by_id(trans, id) users, errors = self._get_users(trans, payload.user_ids) extra, users_to_notify = self._share_with_options(trans, item, users, errors, payload.share_option) @@ -111,7 +114,7 @@ class ShareableService: def _share_with_options( self, - trans, + trans: ProvidesUserContext, item, users: set[User], errors: set[str], @@ -124,19 +127,19 @@ class ShareableService: extra = None return extra, new_users - def _get_item_by_id(self, trans, id: DecodedDatabaseIdField): + def _get_item_by_id(self, trans: ProvidesUserContext, id: DecodedDatabaseIdField): class_name = self.manager.model_class.__name__ item = base.get_object(trans, id, class_name, check_ownership=True, check_accessible=True, deleted=False) return item - def _get_sharing_status(self, trans, item): + def _get_sharing_status(self, trans: ProvidesUserContext, item): status = self.serializer.serialize_to_view( item, user=trans.user, trans=trans, default_view="sharing", encode_id=False ) status["users_shared_with"] = [UserEmail(id=a.user.id, email=a.user.email) for a in item.users_shared_with] return SharingStatus(**status) - def _get_users(self, trans, emails_or_ids: list[UserIdentifier]) -> tuple[set[User], set[str]]: + def _get_users(self, trans: ProvidesUserContext, emails_or_ids: list[UserIdentifier]) -> tuple[set[User], set[str]]: send_to_users: set[User] = set() send_to_err: set[str] = set() for email_or_id in set(emails_or_ids): diff --git a/lib/galaxy/webapps/galaxy/services/tools.py b/lib/galaxy/webapps/galaxy/services/tools.py index 4e3b72ac2ee..0446646ebdc 100644 --- a/lib/galaxy/webapps/galaxy/services/tools.py +++ b/lib/galaxy/webapps/galaxy/services/tools.py @@ -395,7 +395,9 @@ class ToolsService(ServiceBase): ) return self._handle_inputs_output_to_api_response(trans, tool, target_history, vars) - def _handle_inputs_output_to_api_response(self, trans, tool, target_history, vars) -> JobCreateResponse: + def _handle_inputs_output_to_api_response( + self, trans: ProvidesHistoryContext, tool, target_history, vars + ) -> JobCreateResponse: # TODO: check for errors and ensure that output dataset(s) are available. output_datasets = vars.get("out_data", []) rval: dict[str, Any] = {"outputs": [], "output_collections": [], "jobs": [], "implicit_collections": []} @@ -537,7 +539,7 @@ class ToolsService(ServiceBase): detected_versions.append(tool.version) return detected_versions - def get_tool_icon_path(self, trans, tool_id, tool_version=None) -> str | None: + def get_tool_icon_path(self, trans: ProvidesUserContext, tool_id, tool_version=None) -> str | None: tool = self._get_tool(trans, tool_id, tool_version) if tool and tool.icon: icon_file_path = tool.icon diff --git a/lib/galaxy/webapps/galaxy/services/users.py b/lib/galaxy/webapps/galaxy/services/users.py index a32743b99a3..b198d9ed3b3 100644 --- a/lib/galaxy/webapps/galaxy/services/users.py +++ b/lib/galaxy/webapps/galaxy/services/users.py @@ -255,7 +255,7 @@ class UsersService(ServiceBase): rval.append(UserModel(**user_dict)) return rval - def get_user_roles(self, trans, user_id): + def get_user_roles(self, trans: ProvidesUserContext, user_id): user = self.get_user(trans, user_id) roles = [ura.role for ura in user.roles] return RoleListResponse(root=[role_to_model(r) for r in roles]) diff --git a/lib/galaxy/webapps/galaxy/services/wes.py b/lib/galaxy/webapps/galaxy/services/wes.py index a8c72e98c50..5a966eb1eb5 100644 --- a/lib/galaxy/webapps/galaxy/services/wes.py +++ b/lib/galaxy/webapps/galaxy/services/wes.py @@ -25,7 +25,10 @@ from sqlalchemy.orm import joinedload from galaxy import exceptions from galaxy.config import GalaxyAppConfiguration from galaxy.files.uris import stream_url_to_str -from galaxy.managers.context import ProvidesUserContext +from galaxy.managers.context import ( + ProvidesHistoryContext, + ProvidesUserContext, +) from galaxy.managers.workflows import ( RawWorkflowDescription, WorkflowContentsManager, @@ -440,7 +443,7 @@ class WesService(ServiceBase): def submit_run( self, - trans: ProvidesUserContext, + trans: ProvidesHistoryContext, workflow_params: str | None = None, workflow_type: str | None = None, workflow_type_version: str | None = None, diff --git a/lib/galaxy/webapps/galaxy/services/workflows.py b/lib/galaxy/webapps/galaxy/services/workflows.py index baaf3371535..bf6ad69a388 100644 --- a/lib/galaxy/webapps/galaxy/services/workflows.py +++ b/lib/galaxy/webapps/galaxy/services/workflows.py @@ -54,6 +54,7 @@ from galaxy.workflow.extract import ( ) from galaxy.workflow.run import queue_invoke from galaxy.workflow.run_request import build_workflow_run_configs +from galaxy.workflow.scheduling_manager import WorkflowSchedulingManager log = logging.getLogger(__name__) @@ -102,8 +103,10 @@ class WorkflowsService(ServiceBase): tool_shed_registry: Registry, notification_service: NotificationService, job_manager: JobManager, + workflow_scheduling_manager: WorkflowSchedulingManager, ): self._workflows_manager = workflows_manager + self._workflow_scheduling_manager = workflow_scheduling_manager self._workflow_contents_manager = workflow_contents_manager self._serializer = serializer self.shareable_service = ShareableService(workflows_manager, serializer, notification_service) @@ -173,7 +176,7 @@ class WorkflowsService(ServiceBase): def invoke_workflow( self, - trans, + trans: ProvidesHistoryContext, workflow_id, payload: InvokeWorkflowPayload, ) -> WorkflowInvocationResponse | list[WorkflowInvocationResponse]: @@ -202,7 +205,7 @@ class WorkflowsService(ServiceBase): tool["tool_id"], tool_version=tool["tool_version"], tool_uuid=tool["tool_uuid"], - exact=require_exact_tool_versions, + exact=bool(require_exact_tool_versions), user=trans.user, ) ] @@ -225,6 +228,7 @@ class WorkflowsService(ServiceBase): trans=trans, workflow=workflow, workflow_run_config=run_config, + workflow_scheduling_manager=self._workflow_scheduling_manager, request_params=work_request_params, flush=False, ) @@ -370,17 +374,17 @@ class WorkflowsService(ServiceBase): ) seen_labels.add(sanitized_label) - def delete(self, trans, workflow_id): + def delete(self, trans: ProvidesUserContext, workflow_id): workflow_to_delete = self._workflows_manager.get_stored_workflow(trans, workflow_id) self._workflows_manager.check_security(trans, workflow_to_delete) self._workflows_manager.delete(workflow_to_delete) - def undelete(self, trans, workflow_id): + def undelete(self, trans: ProvidesUserContext, workflow_id): workflow_to_undelete = self._workflows_manager.get_stored_workflow(trans, workflow_id) self._workflows_manager.check_security(trans, workflow_to_undelete) self._workflows_manager.undelete(workflow_to_undelete) - def get_versions(self, trans, workflow_id, instance: bool): + def get_versions(self, trans: ProvidesUserContext, workflow_id, instance: bool): stored_workflow: StoredWorkflow = self._workflows_manager.get_stored_accessible_workflow( trans, workflow_id, by_stored_id=not instance ) @@ -389,13 +393,13 @@ class WorkflowsService(ServiceBase): for i, w in enumerate(reversed(stored_workflow.workflows)) ] - def invocation_counts(self, trans, workflow_id, instance: bool) -> InvocationsStateCounts: + def invocation_counts(self, trans: ProvidesUserContext, workflow_id, instance: bool) -> InvocationsStateCounts: stored_workflow: StoredWorkflow = self._workflows_manager.get_stored_accessible_workflow( trans, workflow_id, by_stored_id=not instance ) return stored_workflow.invocation_counts() - def get_workflow_menu(self, trans, payload): + def get_workflow_menu(self, trans: ProvidesUserContext, payload): ids_in_menu = [x.stored_workflow_id for x in trans.user.stored_workflow_menu_entries] workflows = self._get_workflows_list( trans, @@ -405,7 +409,7 @@ class WorkflowsService(ServiceBase): def refactor( self, - trans: ProvidesUserContext, + trans: ProvidesHistoryContext, workflow_id: DecodedDatabaseIdField, payload: RefactorRequest, instance: bool, @@ -413,7 +417,9 @@ class WorkflowsService(ServiceBase): stored_workflow = self._workflows_manager.get_stored_workflow(trans, workflow_id, by_stored_id=not instance) return self._workflow_contents_manager.refactor(trans, stored_workflow, payload) - def show_workflow(self, trans, workflow_id, instance, legacy, version) -> StoredWorkflowDetailed: + def show_workflow( + self, trans: ProvidesHistoryContext, workflow_id, instance, legacy, version + ) -> StoredWorkflowDetailed: stored_workflow = self._workflows_manager.get_stored_workflow(trans, workflow_id, by_stored_id=not instance) if stored_workflow.importable is False and stored_workflow.user != trans.user and not trans.user_is_admin: wf_count = 0 if not trans.user else trans.user.count_stored_workflow_user_assocs(stored_workflow) diff --git a/lib/galaxy/workflow/errors.py b/lib/galaxy/workflow/errors.py index 56291a4ac2d..71f08ce6f01 100644 --- a/lib/galaxy/workflow/errors.py +++ b/lib/galaxy/workflow/errors.py @@ -3,6 +3,10 @@ Functionality for sending error reports for workflow runs. """ import string +from typing import ( + Optional, + TYPE_CHECKING, +) import markupsafe @@ -13,6 +17,9 @@ from galaxy import ( from galaxy.security.validate_user_input import validate_email_str from galaxy.util import unicodify +if TYPE_CHECKING: + from galaxy.managers.context import ProvidesUserContext + error_report_template = """ GALAXY WORKFLOW RUN ERROR REPORT ------------------------ @@ -93,7 +100,7 @@ class WorkflowErrorReporter: self.app = app self.report = None - def _can_access_invocation(self, trans, user): + def _can_access_invocation(self, trans: Optional["ProvidesUserContext"], user): if not user: return False if not trans: @@ -171,7 +178,7 @@ class WorkflowErrorReporter: class WorkflowEmailErrorReporter(WorkflowErrorReporter): - def _send_report(self, user, email=None, message=None, trans=None, **kwd): + def _send_report(self, user, email=None, message=None, trans: Optional["ProvidesUserContext"] = None, **kwd): smtp_server = self.app.config.smtp_server assert smtp_server, ValueError("Mail is not configured for this Galaxy instance") to = self.app.config.error_email_to diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index 32589a21c07..68738705797 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -153,8 +153,13 @@ from galaxy.workflow.workflow_parameter_input_definitions import ( ) if TYPE_CHECKING: - from galaxy.managers.context import ProvidesUserContext + from galaxy.managers.context import ( + ProvidesAppContext, + ProvidesHistoryContext, + ) from galaxy.schema.invocation import InvocationMessageUnion + from galaxy.structured_app import StructuredApp + from galaxy.work.context import WorkRequestContext from galaxy.workflow.run import WorkflowProgress log = logging.getLogger(__name__) @@ -336,7 +341,7 @@ class WorkflowModule: type: str name: str - def __init__(self, trans, content_id=None, **kwds): + def __init__(self, trans: "ProvidesHistoryContext", content_id=None, **kwds): self.trans = trans self.content_id = content_id self.state = DefaultToolState() @@ -344,7 +349,7 @@ class WorkflowModule: # ---- Creating modules from various representations --------------------- @classmethod - def from_dict(Class, trans, d, **kwds): + def from_dict(Class, trans: "ProvidesHistoryContext", d, **kwds): module = Class(trans, **kwds) input_connections = d.get("input_connections", {}) module.recover_state(d.get("tool_state"), input_connections=input_connections, **kwds) @@ -352,7 +357,7 @@ class WorkflowModule: return module @classmethod - def from_workflow_step(Class, trans, step, **kwds): + def from_workflow_step(Class, trans: "ProvidesHistoryContext", step, **kwds): module = Class(trans, **kwds) module.recover_state(step.tool_inputs, from_tool_form=False) module.label = step.label @@ -490,7 +495,9 @@ class WorkflowModule: """ return {} - def compute_runtime_state(self, trans, step=None, step_updates=None, replace_default_values=False): + def compute_runtime_state( + self, trans: "ProvidesHistoryContext", step=None, step_updates=None, replace_default_values=False + ): """Determine the runtime state (potentially different from self.state which describes configuration state). This (again unlike self.state) is currently always a `DefaultToolState` object. @@ -517,6 +524,7 @@ class WorkflowModule: if replace_default_values and step_input.default_value_set: input_value = step_input.default_value if isinstance(input, BaseDataToolParameter): + assert trans.history is not None input_value = raw_to_galaxy(trans.app, trans.history, input_value) return input_value @@ -558,7 +566,11 @@ class WorkflowModule: return state def execute( - self, trans, progress: "WorkflowProgress", invocation_step, use_cached_job: bool = False + self, + trans: "WorkRequestContext", + progress: "WorkflowProgress", + invocation_step, + use_cached_job: bool = False, ) -> bool | None: """Execute the given workflow invocation step. @@ -730,12 +742,12 @@ class SubWorkflowModule(WorkflowModule): _modules: list[Any] | None = None subworkflow: Workflow - def __init__(self, trans, content_id=None, **kwds): + def __init__(self, trans: "ProvidesHistoryContext", content_id=None, **kwds): super().__init__(trans, content_id, **kwds) self.post_job_actions: dict[str, Any] | None = None @classmethod - def from_dict(Class, trans, d, **kwds): + def from_dict(Class, trans: "ProvidesHistoryContext", d, **kwds): module = super().from_dict(trans, d, **kwds) if "subworkflow" in d: detached = kwds.get("detached", False) @@ -748,7 +760,7 @@ class SubWorkflowModule(WorkflowModule): return module @classmethod - def from_workflow_step(Class, trans, step, **kwds): + def from_workflow_step(Class, trans: "ProvidesHistoryContext", step, **kwds): module = super().from_workflow_step(trans, step, **kwds) module.subworkflow = step.subworkflow return module @@ -821,7 +833,8 @@ class SubWorkflowModule(WorkflowModule): if hasattr(self.subworkflow, "workflow_outputs"): from galaxy.managers.workflows import WorkflowContentsManager - workflow_contents_manager = WorkflowContentsManager(self.trans.app, self.trans.app.trs_proxy) + app = cast("StructuredApp", self.trans.app) + workflow_contents_manager = WorkflowContentsManager(app, app.trs_proxy) subworkflow_dict = workflow_contents_manager._workflow_to_dict_editor( trans=self.trans, stored=self.subworkflow.stored_workflow, @@ -871,7 +884,11 @@ class SubWorkflowModule(WorkflowModule): return self.trans.security.encode_id(self.subworkflow.id) def execute( - self, trans, progress: "WorkflowProgress", invocation_step: WorkflowInvocationStep, use_cached_job: bool = False + self, + trans: "WorkRequestContext", + progress: "WorkflowProgress", + invocation_step: WorkflowInvocationStep, + use_cached_job: bool = False, ) -> bool | None: """Execute the given workflow step in the given workflow invocation. Use the supplied workflow progress object to track outputs, find @@ -1022,7 +1039,7 @@ def optional_param(optional=None): return optional_value -def format_param(trans, formats): +def format_param(trans: "ProvidesAppContext", formats): formats_val = "" if not formats else ",".join(formats) source = dict( type="text", @@ -1056,7 +1073,11 @@ class InputModule(WorkflowModule): return [] def execute( - self, trans, progress: "WorkflowProgress", invocation_step, use_cached_job: bool = False + self, + trans: "WorkRequestContext", + progress: "WorkflowProgress", + invocation_step, + use_cached_job: bool = False, ) -> bool | None: invocation = invocation_step.workflow_invocation step = invocation_step.workflow_step @@ -1064,6 +1085,7 @@ class InputModule(WorkflowModule): if input_value is NO_REPLACEMENT: default_value = step.get_input_default_value(NO_REPLACEMENT) if default_value is not NO_REPLACEMENT: + assert trans.history is not None input_value = raw_to_galaxy(trans.app, trans.history, default_value) step_outputs = dict(output=input_value) @@ -1698,7 +1720,7 @@ class InputParameterModule(WorkflowModule): def execute( self, - trans, + trans: "WorkRequestContext", progress: "WorkflowProgress", invocation_step: "WorkflowInvocationStep", use_cached_job: bool = False, @@ -1933,7 +1955,11 @@ class PauseModule(WorkflowModule): return state def execute( - self, trans, progress: "WorkflowProgress", invocation_step, use_cached_job: bool = False + self, + trans: "WorkRequestContext", + progress: "WorkflowProgress", + invocation_step, + use_cached_job: bool = False, ) -> bool | None: step = invocation_step.workflow_step progress.mark_step_outputs_delayed(step, why="executing pause step") @@ -1980,18 +2006,18 @@ class PickValueModule(WorkflowModule): MODES = ("first_non_null", "first_or_skip", "the_only_non_null", "all_non_null") - def __init__(self, trans, content_id=None, **kwds): + def __init__(self, trans: "ProvidesHistoryContext", content_id=None, **kwds): super().__init__(trans, content_id=content_id, **kwds) self.post_job_actions: dict[str, Any] = {} @classmethod - def from_dict(Class, trans, d, **kwds): + def from_dict(Class, trans: "ProvidesHistoryContext", d, **kwds): module = super().from_dict(trans, d, **kwds) module.post_job_actions = d.get("post_job_actions", {}) return module @classmethod - def from_workflow_step(Class, trans, step, **kwds): + def from_workflow_step(Class, trans: "ProvidesHistoryContext", step, **kwds): module = super().from_workflow_step(trans, step, **kwds) module.post_job_actions = {} for pja in step.post_job_actions: @@ -2086,7 +2112,7 @@ class PickValueModule(WorkflowModule): return True return False - def _pick_from_replacements(self, trans, invocation_step, mode, replacements): + def _pick_from_replacements(self, trans: "ProvidesHistoryContext", invocation_step, mode, replacements): """Apply pick logic to a list of replacement values. Returns the picked output.""" step = invocation_step.workflow_step non_null = [r for r in replacements if not self._is_null_or_skipped(r)] @@ -2123,7 +2149,11 @@ class PickValueModule(WorkflowModule): raise ValueError(f"Unknown pick_value mode: {mode}") def execute( - self, trans, progress: "WorkflowProgress", invocation_step, use_cached_job: bool = False + self, + trans: "WorkRequestContext", + progress: "WorkflowProgress", + invocation_step, + use_cached_job: bool = False, ) -> bool | None: step = invocation_step.workflow_step mode = step.tool_inputs.get("mode", "first_non_null") if step.tool_inputs else "first_non_null" @@ -2146,7 +2176,7 @@ class PickValueModule(WorkflowModule): self._apply_post_job_actions(trans, step, output, progress.effective_replacement_dict()) return None - def _execute_mapped(self, trans, invocation_step, mode, all_inputs, collection_info): + def _execute_mapped(self, trans: "ProvidesHistoryContext", invocation_step, mode, all_inputs, collection_info): """Execute pick_value mapped over collection inputs.""" invocation = invocation_step.workflow_invocation history = invocation.history @@ -2185,7 +2215,7 @@ class PickValueModule(WorkflowModule): # Build the output collection from per-element outputs return self._create_mapped_output_collection(trans, history, mode, per_element_outputs) - def _create_skipped_output(self, trans, invocation_step): + def _create_skipped_output(self, trans: "ProvidesHistoryContext", invocation_step): """Create a skipped HDA for first_or_skip when all inputs are null.""" invocation = invocation_step.workflow_invocation history = invocation.history @@ -2201,7 +2231,7 @@ class PickValueModule(WorkflowModule): trans.sa_session.add(hda) return hda - def _create_collection_from_list(self, trans, invocation_step, hdas): + def _create_collection_from_list(self, trans: "ProvidesHistoryContext", invocation_step, hdas): """Create an HDCA from a list of non-null HDAs for all_non_null mode.""" invocation = invocation_step.workflow_invocation history = invocation.history @@ -2224,7 +2254,7 @@ class PickValueModule(WorkflowModule): ) return hdca - def _create_mapped_output_collection(self, trans, history, mode, per_element_outputs): + def _create_mapped_output_collection(self, trans: "ProvidesHistoryContext", history, mode, per_element_outputs): """Create an implicit output collection from per-element pick results. For single-value modes (first_non_null, etc.), creates a flat list of HDAs. @@ -2268,7 +2298,7 @@ class PickValueModule(WorkflowModule): element_identifiers=elements, ) - def _apply_post_job_actions(self, trans, step, output, replacement_dict): + def _apply_post_job_actions(self, trans: "ProvidesAppContext", step, output, replacement_dict): """Apply post job actions directly to module output via ActionBox. Uses execute_on_mapped_over which operates on step_outputs dict @@ -2332,7 +2362,7 @@ def _mapped_inputs_from_collection_info(collection_info) -> dict[str, MappedColl def _capture_workflow_tool_request_state( - trans, + trans: "ProvidesAppContext", tool, step, collection_info, @@ -2461,7 +2491,7 @@ def _capture_workflow_tool_request_state( def _log_workflow_tool_request_state( - trans, tool, step, collection_info, request_state: WorkflowToolRequestState + trans: "ProvidesAppContext", tool, step, collection_info, request_state: WorkflowToolRequestState ) -> None: mapped_over = bool(getattr(collection_info, "collections", None)) log.info( @@ -2482,7 +2512,7 @@ class ToolModule(WorkflowModule): name = "Tool" def __init__( - self, trans: "ProvidesUserContext", tool_id, tool_version=None, exact_tools=True, tool_uuid=None, **kwds + self, trans: "ProvidesHistoryContext", tool_id, tool_version=None, exact_tools=True, tool_uuid=None, **kwds ): super().__init__(trans, content_id=tool_id, **kwds) self.tool_id = tool_id @@ -2517,7 +2547,7 @@ class ToolModule(WorkflowModule): # ---- Creating modules from various representations --------------------- @classmethod - def from_dict(Class, trans: "ProvidesUserContext", d, **kwds): + def from_dict(Class, trans: "ProvidesHistoryContext", d, **kwds): tool_id = d.get("content_id") or d.get("tool_id") tool_version = d.get("tool_version") if tool_version: @@ -2569,7 +2599,7 @@ class ToolModule(WorkflowModule): return module @classmethod - def from_workflow_step(Class, trans, step, **kwds): + def from_workflow_step(Class, trans: "ProvidesHistoryContext", step, **kwds): tool_version = step.tool_version tool_uuid = step.tool_uuid kwds["exact_tools"] = False @@ -2624,7 +2654,7 @@ class ToolModule(WorkflowModule): step.tool_version = self.tool_version if tool_uuid := getattr(self, "tool_uuid", None): tool = self.trans.app.toolbox.get_tool(tool_uuid=tool_uuid, user=self.trans.user) - if tool: + if tool and tool.dynamic_tool: step.dynamic_tool_id = tool.dynamic_tool.id if not detached: for k, v in self.post_job_actions.items(): @@ -2644,6 +2674,7 @@ class ToolModule(WorkflowModule): def get_tooltip(self, static_path=None): if self.tool and self.tool.raw_help and self.tool.raw_help.format == "restructuredtext": + assert self.trans.url_builder is not None host_url = self.trans.url_builder("/") static_path = self.trans.url_builder(static_path) if static_path else "" return self.tool.render_help(host_url=host_url, static_path=static_path) @@ -2903,7 +2934,9 @@ class ToolModule(WorkflowModule): def get_runtime_inputs(self, step, connections: Iterable[WorkflowStepConnection] | None = None): return self.get_inputs() - def compute_runtime_state(self, trans, step=None, step_updates=None, replace_default_values=False): + def compute_runtime_state( + self, trans: "ProvidesHistoryContext", step=None, step_updates=None, replace_default_values=False + ): # Warning: This method destructively modifies existing step state. if self.tool: step_errors = {} @@ -2938,7 +2971,7 @@ class ToolModule(WorkflowModule): def execute( self, - trans, + trans: "WorkRequestContext", progress: "WorkflowProgress", invocation_step: "WorkflowInvocationStep", use_cached_job: bool = False, @@ -2948,6 +2981,10 @@ class ToolModule(WorkflowModule): tool = trans.app.toolbox.get_tool( step.tool_id, tool_version=step.tool_version, tool_uuid=step.tool_uuid, user=trans.user ) + if tool is None: + raise ToolMissingException( + f"Tool {step.tool_id} missing. Cannot execute workflow step.", tool_id=step.tool_id + ) if not tool.is_workflow_compatible: # TODO: why do we even create an invocation, seems like something we could check on submit? message = f"Specified tool [{tool.id}] in step {step.order_index + 1} is not workflow-compatible." @@ -3144,7 +3181,7 @@ class ToolModule(WorkflowModule): credentials_context = self._resolve_credentials_context(tool) execution_tracker = execute( - trans=self.trans, + trans=trans, tool=tool, mapping_params=mapping_params, history=invocation.history, @@ -3210,7 +3247,7 @@ class ToolModule(WorkflowModule): return complete @staticmethod - def _build_step_error_failure(trans, step, step_errors, progress): + def _build_step_error_failure(trans: "ProvidesHistoryContext", step, step_errors, progress): """Build the appropriate invocation failure message for step parameter errors. Inspects the ParameterValueError objects to determine whether the error @@ -3341,7 +3378,7 @@ class WorkflowModuleFactory: def __init__(self, module_types: dict[str, type[WorkflowModule]]): self.module_types = module_types - def from_dict(self, trans, d, **kwargs) -> WorkflowModule: + def from_dict(self, trans: "ProvidesHistoryContext", d, **kwargs) -> WorkflowModule: """ Return module initialized from the data in dictionary `d`. """ @@ -3351,7 +3388,7 @@ class WorkflowModuleFactory: ), f"Unexpected workflow step type [{type}] not found in [{self.module_types.keys()}]" return self.module_types[type].from_dict(trans, d, **kwargs) - def from_workflow_step(self, trans, step: WorkflowStep, **kwargs) -> WorkflowModule: + def from_workflow_step(self, trans: "ProvidesHistoryContext", step: WorkflowStep, **kwargs) -> WorkflowModule: """ Return module initialized from the WorkflowStep object `step`. """ @@ -3395,7 +3432,7 @@ class WorkflowModuleInjector: """Injects workflow step objects from the ORM with appropriate module and module generated/influenced state.""" - def __init__(self, trans, allow_tool_state_corrections=False): + def __init__(self, trans: "ProvidesHistoryContext", allow_tool_state_corrections=False): self.trans = trans self.allow_tool_state_corrections = allow_tool_state_corrections @@ -3461,7 +3498,11 @@ class WorkflowModuleInjector: def populate_module_and_state( - trans, workflow: Workflow, param_map, allow_tool_state_corrections=False, module_injector=None + trans: "ProvidesHistoryContext", + workflow: Workflow, + param_map, + allow_tool_state_corrections=False, + module_injector=None, ): """Used by API but not web controller, walks through a workflow's steps and populates transient module and state attributes on each. diff --git a/lib/galaxy/workflow/reports/__init__.py b/lib/galaxy/workflow/reports/__init__.py index 866f17c28b4..55ce4202a9b 100644 --- a/lib/galaxy/workflow/reports/__init__.py +++ b/lib/galaxy/workflow/reports/__init__.py @@ -1,10 +1,17 @@ +from typing import TYPE_CHECKING + from galaxy.exceptions import RequestParameterInvalidException from galaxy.util import plugin_config +if TYPE_CHECKING: + from galaxy.managers.context import ProvidesUserContext + DEFAULT_REPORT_GENERATOR_TYPE = "markdown" -def generate_report(trans, invocation, runtime_report_config_json=None, plugin_type=None, target_format="json"): +def generate_report( + trans: "ProvidesUserContext", invocation, runtime_report_config_json=None, plugin_type=None, target_format="json" +): import galaxy.workflow.reports.generators plugin_classes = plugin_config.plugins_dict(galaxy.workflow.reports.generators, "plugin_type") diff --git a/lib/galaxy/workflow/reports/generators/__init__.py b/lib/galaxy/workflow/reports/generators/__init__.py index ddc02571274..7a62dd28722 100644 --- a/lib/galaxy/workflow/reports/generators/__init__.py +++ b/lib/galaxy/workflow/reports/generators/__init__.py @@ -4,6 +4,7 @@ from abc import ( ABCMeta, abstractmethod, ) +from typing import TYPE_CHECKING from galaxy.managers import workflows from galaxy.managers.markdown_util import ( @@ -14,6 +15,9 @@ from galaxy.managers.markdown_util import ( from galaxy.model import WorkflowInvocation from galaxy.schema import PdfDocumentType +if TYPE_CHECKING: + from galaxy.managers.context import ProvidesHistoryContext + class WorkflowReportGeneratorPlugin(metaclass=ABCMeta): """ """ @@ -24,18 +28,20 @@ class WorkflowReportGeneratorPlugin(metaclass=ABCMeta): """Short string labelling this plugin.""" @abstractmethod - def generate_report_json(self, trans, invocation, runtime_report_config_json=None): + def generate_report_json(self, trans: "ProvidesHistoryContext", invocation, runtime_report_config_json=None): """ """ @abstractmethod - def generate_report_pdf(self, trans, invocation, runtime_report_config_json=None): + def generate_report_pdf(self, trans: "ProvidesHistoryContext", invocation, runtime_report_config_json=None): """ """ class WorkflowMarkdownGeneratorPlugin(WorkflowReportGeneratorPlugin, metaclass=ABCMeta): """WorkflowReportGeneratorPlugin that generates markdown as base report.""" - def generate_report_json(self, trans, invocation: WorkflowInvocation, runtime_report_config_json=None): + def generate_report_json( + self, trans: "ProvidesHistoryContext", invocation: WorkflowInvocation, runtime_report_config_json=None + ): """ """ workflow_manager = workflows.WorkflowsManager(trans.app) workflow_encoded_id = trans.app.security.encode_id(invocation.workflow_id) @@ -60,17 +66,17 @@ class WorkflowMarkdownGeneratorPlugin(WorkflowReportGeneratorPlugin, metaclass=A rval.update(extra_rendering_data) return rval - def generate_report_pdf(self, trans, invocation, runtime_report_config_json=None): + def generate_report_pdf(self, trans: "ProvidesHistoryContext", invocation, runtime_report_config_json=None): internal_markdown = self._generate_internal_markdown( trans, invocation, runtime_report_config_json=runtime_report_config_json ) return internal_galaxy_markdown_to_pdf(trans, internal_markdown, PdfDocumentType.invocation_report) @abstractmethod - def _generate_report_markdown(self, trans, invocation, runtime_report_config_json=None): + def _generate_report_markdown(self, trans: "ProvidesHistoryContext", invocation, runtime_report_config_json=None): """ """ - def _generate_internal_markdown(self, trans, invocation, runtime_report_config_json=None): + def _generate_internal_markdown(self, trans: "ProvidesHistoryContext", invocation, runtime_report_config_json=None): workflow_markdown = self._generate_report_markdown( trans, invocation, runtime_report_config_json=runtime_report_config_json ) diff --git a/lib/galaxy/workflow/reports/generators/markdown.py b/lib/galaxy/workflow/reports/generators/markdown.py index 49f4e92b858..9df50ec1327 100644 --- a/lib/galaxy/workflow/reports/generators/markdown.py +++ b/lib/galaxy/workflow/reports/generators/markdown.py @@ -2,9 +2,13 @@ import logging import string +from typing import TYPE_CHECKING from . import WorkflowMarkdownGeneratorPlugin +if TYPE_CHECKING: + from galaxy.managers.context import ProvidesHistoryContext + log = logging.getLogger(__name__) DEFAULT_MARKDOWN = """ @@ -30,7 +34,7 @@ workflow_display() class MarkdownWorkflowMarkdownReportGeneratorPlugin(WorkflowMarkdownGeneratorPlugin): plugin_type = "markdown" - def _generate_report_markdown(self, trans, invocation, runtime_report_config_json=None): + def _generate_report_markdown(self, trans: "ProvidesHistoryContext", invocation, runtime_report_config_json=None): reports_config = (invocation.workflow.reports_config or {}).copy() # TODO: more intelligent merge here. reports_config.update(runtime_report_config_json or {}) diff --git a/lib/galaxy/workflow/resources/__init__.py b/lib/galaxy/workflow/resources/__init__.py index 13e55653188..4cdf8a24067 100644 --- a/lib/galaxy/workflow/resources/__init__.py +++ b/lib/galaxy/workflow/resources/__init__.py @@ -9,11 +9,18 @@ import logging import os import sys from copy import deepcopy +from typing import ( + Any, + TYPE_CHECKING, +) import yaml import galaxy.util +if TYPE_CHECKING: + from galaxy.managers.context import ProvidesUserContext + log = logging.getLogger(__name__) @@ -56,14 +63,14 @@ def _read_defined_parameter_definitions(config): return {} -def _resource_parameters_by_group(trans, **kwds): +def _resource_parameters_by_group(trans: "ProvidesUserContext", **kwds): user = trans.user by_group = kwds["by_group"] workflow_resource_params = kwds["workflow_resource_params"] params = [] if validate_by_group_workflow_parameters_mapper(by_group, workflow_resource_params): - user_permissions = {} + user_permissions: dict[Any, dict] = {} user_groups = [] for g in user.groups: user_groups.append(g.group.name) @@ -74,7 +81,7 @@ def _resource_parameters_by_group(trans, **kwds): if isinstance(tag, dict): if tag.get("name") not in user_permissions: user_permissions[tag.get("name")] = {} - for option in tag.get("options"): + for option in tag.get("options") or []: user_permissions[tag.get("name")][option] = {} else: if tag not in user_permissions: diff --git a/lib/galaxy/workflow/run.py b/lib/galaxy/workflow/run.py index 2156f24c986..ff6500c19f2 100644 --- a/lib/galaxy/workflow/run.py +++ b/lib/galaxy/workflow/run.py @@ -46,6 +46,7 @@ from galaxy.workflow.run_request import ( ) if TYPE_CHECKING: + from galaxy.managers.context import ProvidesHistoryContext from galaxy.model import ( HistoryItem, Workflow, @@ -53,8 +54,8 @@ if TYPE_CHECKING: WorkflowStep, WorkflowStepConnection, ) - from galaxy.webapps.base.webapp import GalaxyWebTransaction from galaxy.work.context import WorkRequestContext + from galaxy.workflow.scheduling_manager import WorkflowSchedulingManager log = logging.getLogger(__name__) @@ -125,9 +126,10 @@ def __invoke( def queue_invoke( - trans: "GalaxyWebTransaction", + trans: "ProvidesHistoryContext", workflow: "Workflow", workflow_run_config: WorkflowRunConfig, + workflow_scheduling_manager: "WorkflowSchedulingManager", request_params: dict[str, Any] | None = None, populate_state: bool = True, flush: bool = True, @@ -145,7 +147,7 @@ def queue_invoke( initial_state = model.WorkflowInvocation.states.NEW if workflow_run_config.requires_materialization: initial_state = model.WorkflowInvocation.states.REQUIRES_MATERIALIZATION - return trans.app.workflow_scheduling_manager.queue( + return workflow_scheduling_manager.queue( workflow_invocation, request_params, flush=flush, initial_state=initial_state ) @@ -375,7 +377,7 @@ STEP_OUTPUT_DELAYED = object() class ModuleInjector(Protocol): - trans: "WorkRequestContext" + trans: "ProvidesHistoryContext" def inject(self, step, step_args=None, steps=None, **kwargs): pass @@ -458,7 +460,7 @@ class WorkflowProgress: remaining_steps.append((step, invocation_step)) return remaining_steps - def replacement_for_input(self, trans, step: "WorkflowStep", input_dict: dict[str, Any]): + def replacement_for_input(self, trans: "ProvidesHistoryContext", step: "WorkflowStep", input_dict: dict[str, Any]): replacement: ( NoReplacement | model.DatasetCollectionInstance | list[model.DatasetCollectionInstance] | HistoryItem ) = NO_REPLACEMENT @@ -492,6 +494,7 @@ class WorkflowProgress: elif (step_input := step.inputs_by_name.get(prefixed_name)) and step_input.default_value_set: replacement = step_input.default_value if is_data: + assert trans.history is not None replacement = raw_to_galaxy(trans.app, trans.history, step_input.default_value) return replacement @@ -825,7 +828,9 @@ class WorkflowProgress: ) def raw_to_galaxy(self, value: dict): - return raw_to_galaxy(self.module_injector.trans.app, self.module_injector.trans.history, value) + trans = self.module_injector.trans + assert trans.history is not None + return raw_to_galaxy(trans.app, trans.history, value) def _recover_mapping(self, step_invocation: WorkflowInvocationStep) -> None: assert step_invocation.workflow_step.module diff --git a/lib/galaxy/workflow/run_request.py b/lib/galaxy/workflow/run_request.py index 4996ea9aaa6..37f8886df1e 100644 --- a/lib/galaxy/workflow/run_request.py +++ b/lib/galaxy/workflow/run_request.py @@ -40,11 +40,11 @@ from galaxy.workflow.modules import WorkflowModuleInjector from galaxy.workflow.resources import get_resource_mapper_function if TYPE_CHECKING: + from galaxy.managers.context import ProvidesHistoryContext from galaxy.model import ( Workflow, WorkflowStep, ) - from galaxy.webapps.base.webapp import GalaxyWebTransaction INPUT_STEP_TYPES = ["data_input", "data_collection_input", "parameter_input"] @@ -261,7 +261,7 @@ def _flatten_step_params(param_dict: dict, prefix: str = "") -> dict: def _get_target_history( - trans: "GalaxyWebTransaction", + trans: "ProvidesHistoryContext", workflow: "Workflow", payload: dict[str, Any], param_keys: list[list] | None = None, @@ -306,7 +306,7 @@ def _get_target_history( def build_workflow_run_configs( - trans: "GalaxyWebTransaction", workflow: "Workflow", payload: dict[str, Any] + trans: "ProvidesHistoryContext", workflow: "Workflow", payload: dict[str, Any] ) -> list[WorkflowRunConfig]: app = trans.app allow_tool_state_corrections = payload.get("allow_tool_state_corrections", False) @@ -532,7 +532,7 @@ def build_workflow_run_configs( def workflow_run_config_to_request( - trans: "GalaxyWebTransaction", run_config: WorkflowRunConfig, workflow: "Workflow" + trans: "ProvidesHistoryContext", run_config: WorkflowRunConfig, workflow: "Workflow" ) -> WorkflowInvocation: param_types = WorkflowRequestInputParameter.types diff --git a/lib/tool_shed/managers/groups.py b/lib/tool_shed/managers/groups.py index aa5d73cda85..686898af63a 100644 --- a/lib/tool_shed/managers/groups.py +++ b/lib/tool_shed/managers/groups.py @@ -22,6 +22,7 @@ from galaxy.exceptions import ( ObjectNotFound, RequestParameterInvalidException, ) +from tool_shed.context import ProvidesUserContext from tool_shed.webapp.model import Group log = logging.getLogger(__name__) @@ -35,7 +36,7 @@ class GroupManager: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def get(self, trans, decoded_group_id=None, name=None): + def get(self, trans: ProvidesUserContext, decoded_group_id=None, name=None): """ Get the group from the DB based on its ID or name. @@ -61,7 +62,7 @@ class GroupManager: raise InternalServerError("Error loading from the database.") return group - def create(self, trans, name, description=""): + def create(self, trans: ProvidesUserContext, name, description=""): """ Create a new group. """ @@ -76,7 +77,7 @@ class GroupManager: trans.sa_session.commit() return group - def update(self, trans, group, name=None, description=None): + def update(self, trans: ProvidesUserContext, group, name=None, description=None): """ Update the given group """ @@ -96,7 +97,7 @@ class GroupManager: trans.sa_session.commit() return group - def delete(self, trans, group, undelete=False): + def delete(self, trans: ProvidesUserContext, group, undelete=False): """ Mark given group deleted/undeleted based on the flag. """ @@ -110,7 +111,7 @@ class GroupManager: trans.sa_session.commit() return group - def list(self, trans, deleted=False): + def list(self, trans: ProvidesUserContext, deleted=False): """ Return a list of groups from the DB. diff --git a/lib/tool_shed/managers/repositories.py b/lib/tool_shed/managers/repositories.py index 6baead9f5c9..0b9b78ce8fa 100644 --- a/lib/tool_shed/managers/repositories.py +++ b/lib/tool_shed/managers/repositories.py @@ -522,7 +522,7 @@ def readmes(app: ToolShedApp, repository: Repository, changeset_revision: str) - def reset_metadata_on_repository( - trans: ProvidesUserContext, + trans: ProvidesRepositoriesContext, repository_id, dry_run: bool = False, verbose: bool = False, @@ -530,7 +530,14 @@ def reset_metadata_on_repository( ) -> ResetMetadataOnRepositoryResponse: app: ToolShedApp = trans.app - def handle_repository(trans, start_time, repository, dry_run: bool, verbose: bool, clone_url: str | None = None): + def handle_repository( + trans: ProvidesRepositoriesContext, + start_time, + repository, + dry_run: bool, + verbose: bool, + clone_url: str | None = None, + ): results: dict = dict(start_time=start_time, repository_status=[], dry_run=dry_run) regenerated_metadata = {} try: @@ -603,7 +610,7 @@ def reset_metadata_on_repositories( trans: ProvidesRepositoriesContext, request: ResetMetadataOnRepositoriesRequest ) -> ResetMetadataOnRepositoriesResponse: - def handle_repository(trans, repository, results): + def handle_repository(trans: ProvidesRepositoriesContext, repository, results): log.debug(f"Resetting metadata on repository {repository.name}") try: rmm = repository_metadata_manager.RepositoryMetadataManager( diff --git a/lib/tool_shed/managers/users.py b/lib/tool_shed/managers/users.py index 070c3dcb50f..5b54bf170b6 100644 --- a/lib/tool_shed/managers/users.py +++ b/lib/tool_shed/managers/users.py @@ -65,6 +65,9 @@ def _validate(trans: ProvidesUserContext, email: str, password: str, confirm: st return f"The term '{username}' is a reserved word in the Tool Shed, so it cannot be used as a public user name." message = "\n".join( ( + # tool_shed.context.ProvidesUserContext is a structurally analogous but + # nominally distinct hierarchy from galaxy.managers.context.ProvidesAppContext; + # trans satisfies everything these helpers actually use (.app, .sa_session). validate_email(trans, email), validate_password(trans, password, confirm), validate_publicname(trans, username), diff --git a/lib/tool_shed/webapp/api2/__init__.py b/lib/tool_shed/webapp/api2/__init__.py index bf30152de0f..a98c413bd8d 100644 --- a/lib/tool_shed/webapp/api2/__init__.py +++ b/lib/tool_shed/webapp/api2/__init__.py @@ -353,7 +353,10 @@ def ensure_valid_session(trans: SessionRequestContext) -> None: galaxy_session = None # No relevant cookies, or couldn't find, or invalid, so create a new session if galaxy_session is None: - galaxy_session = create_new_session(trans, prev_galaxy_session, user_for_new_session) + # tool_shed.context.SessionRequestContext structurally satisfies everything + # create_new_session uses (.security, .app, .request) but is a distinct + # hierarchy from galaxy.webapps.base.webapp.GalaxyWebTransaction. + galaxy_session = create_new_session(trans, prev_galaxy_session, user_for_new_session) # type: ignore[arg-type] galaxy_session_requires_flush = True trans.set_galaxy_session(galaxy_session) set_auth_cookie(trans, galaxy_session) diff --git a/lib/tool_shed/webapp/api2/users.py b/lib/tool_shed/webapp/api2/users.py index 842d008c2c6..ab1d70a02ea 100644 --- a/lib/tool_shed/webapp/api2/users.py +++ b/lib/tool_shed/webapp/api2/users.py @@ -326,7 +326,7 @@ def handle_user_login(trans: SessionRequestContext, user: SaUser) -> None: replace_previous_session(trans, user) -def handle_user_logout(trans, logout_all=False): +def handle_user_logout(trans: SessionRequestContext, logout_all=False): """ Logout the current user: - invalidate current session + previous sessions (optional) @@ -339,13 +339,16 @@ def handle_user_logout(trans, logout_all=False): replace_previous_session(trans, None) -def replace_previous_session(trans, user): +def replace_previous_session(trans: SessionRequestContext, user): prev_galaxy_session = trans.get_galaxy_session() # Invalidate previous session if prev_galaxy_session: prev_galaxy_session.is_valid = False # Create new session - new_session = create_new_session(trans, prev_galaxy_session, user) + # tool_shed.context.SessionRequestContext structurally satisfies everything + # create_new_session uses (.security, .app, .request) but is a distinct + # hierarchy from galaxy.webapps.base.webapp.GalaxyWebTransaction. + new_session = create_new_session(trans, prev_galaxy_session, user) # type: ignore[arg-type] trans.set_galaxy_session(new_session) trans.sa_session.add_all((prev_galaxy_session, new_session)) trans.sa_session.commit() diff --git a/lib/tool_shed/webapp/controllers/hg.py b/lib/tool_shed/webapp/controllers/hg.py index a2b4cf8a323..85d9aeb155f 100644 --- a/lib/tool_shed/webapp/controllers/hg.py +++ b/lib/tool_shed/webapp/controllers/hg.py @@ -5,6 +5,7 @@ from mercurial.hgweb.hgwebdir_mod import hgwebdir from galaxy import web from galaxy.exceptions import ObjectNotFound from galaxy.webapps.base.controller import BaseUIController +from galaxy.webapps.base.webapp import GalaxyWebTransaction from tool_shed.webapp.model.db import get_repository_by_name_and_owner log = logging.getLogger(__name__) @@ -21,7 +22,7 @@ class PortAsStringMiddleware: class HgController(BaseUIController): @web.expose - def handle_request(self, trans, **kwd): + def handle_request(self, trans: GalaxyWebTransaction, **kwd): # The os command that results in this method being called will look something like: # hg clone http://test@127.0.0.1:9009/repos/test/convert_characters1 hgweb_config = trans.app.hgweb_config_manager.hgweb_config diff --git a/lib/tool_shed/webapp/search/repo_search.py b/lib/tool_shed/webapp/search/repo_search.py index b3fbb2edadb..24ffa001832 100644 --- a/lib/tool_shed/webapp/search/repo_search.py +++ b/lib/tool_shed/webapp/search/repo_search.py @@ -1,6 +1,7 @@ """Module for searching the toolshed repositories""" import logging +from typing import Any import whoosh.index from whoosh import scoring @@ -21,6 +22,7 @@ from whoosh.query import ( from galaxy import exceptions from galaxy.exceptions import ObjectNotFound from galaxy.util.search import parse_filters +from tool_shed.context import ProvidesAppContext log = logging.getLogger(__name__) @@ -72,7 +74,7 @@ class RepoWeighting(scoring.BM25F): class RepoSearch: - def search(self, trans, search_term, page, page_size, boosts): + def search(self, trans: ProvidesAppContext, search_term, page, page_size, boosts): """ Perform the search on the given search_term @@ -137,7 +139,7 @@ class RepoSearch: log.debug(f"scored hits: {str(hits.scored_length())}") except ValueError: raise ObjectNotFound("The requested page does not exist.") - results = {} + results: dict[str, Any] = {} results["total_results"] = str(len(hits)) results["page"] = str(page) results["page_size"] = str(page_size) diff --git a/lib/tool_shed/webapp/util/ratings_util.py b/lib/tool_shed/webapp/util/ratings_util.py index de338b09135..7ab44036880 100644 --- a/lib/tool_shed/webapp/util/ratings_util.py +++ b/lib/tool_shed/webapp/util/ratings_util.py @@ -1,6 +1,7 @@ import logging from galaxy.model.item_attrs import UsesItemRatings +from tool_shed.context import ProvidesUserContext log = logging.getLogger(__name__) @@ -8,7 +9,7 @@ log = logging.getLogger(__name__) class ItemRatings(UsesItemRatings): """Overrides rate_item method since we also allow for comments""" - def rate_item(self, trans, user, item, rating, comment=""): + def rate_item(self, trans: ProvidesUserContext, user, item, rating, comment=""): """Rate an item. Return type is RatingAssociation.""" item_rating = self.get_user_item_rating(trans.sa_session, user, item, webapp_model=trans.model) if not item_rating: diff --git a/test/integration/test_agents.py b/test/integration/test_agents.py index 42924bf8671..1790ea490a1 100644 --- a/test/integration/test_agents.py +++ b/test/integration/test_agents.py @@ -14,6 +14,7 @@ For deterministic tests without LLM, see test_static_agent_backend.py. import asyncio import logging import os +from typing import cast import pytest from fastmcp import ( @@ -26,6 +27,7 @@ from galaxy.agents.operations import AgentOperationsManager from galaxy.managers.context import ProvidesUserContext from galaxy.util.unittest_utils import pytestmark_live_llm from galaxy.webapps.galaxy.api.mcp import get_mcp_app +from galaxy.work.context import SessionRequestContext from galaxy_test.base.populators import ( DatasetPopulator, TOOL_WITH_SHELL_COMMAND, @@ -216,7 +218,8 @@ class TestAgentOperationsManagerEncoding(AgentIntegrationTestCase): def user_is_admin(self): return False - trans = MinimalTrans(self._app) + # the double only needs security.encode_id for _encode_ids_in_response + trans = cast(SessionRequestContext, MinimalTrans(self._app)) return AgentOperationsManager(app=self._app, trans=trans) def test_encode_ids_helper_encodes_nested_ids(self): diff --git a/test/unit/app/managers/test_UserManager.py b/test/unit/app/managers/test_UserManager.py index 4c2ca1231a0..5e0b26239da 100644 --- a/test/unit/app/managers/test_UserManager.py +++ b/test/unit/app/managers/test_UserManager.py @@ -4,6 +4,10 @@ User Manager testing. Executable directly using: python -m test.unit.managers.test_UserManager """ +from typing import ( + cast, + TYPE_CHECKING, +) from unittest.mock import patch from sqlalchemy import ( @@ -24,6 +28,9 @@ from galaxy.security.passwords import check_password from galaxy.util import now from .base import BaseTestCase +if TYPE_CHECKING: + from galaxy.webapps.base.webapp import GalaxyWebTransaction + # ============================================================================= default_password = "123456" changed_password = "654321" @@ -95,7 +102,7 @@ class TestUserManager(BaseTestCase): def test_trimming(self): self.log("emails must be trimmed") user2b, message = self.user_manager.register( - self.trans, + cast("GalaxyWebTransaction", self.trans), email=" user2b@user2.user2 ", username="user2b", password=default_password, @@ -105,7 +112,7 @@ class TestUserManager(BaseTestCase): assert user2b.email == "user2b@user2.user2" self.log("usernames must be trimmed") user2c, message = self.user_manager.register( - self.trans, + cast("GalaxyWebTransaction", self.trans), email="user2c@user2.user2", username=" user2c ", password=default_password, @@ -289,7 +296,9 @@ class TestUserManager(BaseTestCase): with patch("galaxy.util.send_mail", side_effect=validate_send_email) as mock_send_mail: with patch("galaxy.model.unique_id", return_value="reset_token") as mock_unique_id: - result = self.user_manager.send_reset_email(self.trans, dict(email="user@nopassword.com")) + result = self.user_manager.send_reset_email( + cast("GalaxyWebTransaction", self.trans), dict(email="user@nopassword.com") + ) mock_send_mail.assert_called_once() mock_unique_id.assert_called_once() assert result is None @@ -301,7 +310,7 @@ class TestUserManager(BaseTestCase): user = self.user_manager.create(email=user_email, username="nopassword") self.user_manager.delete(user) assert user.deleted is True - message = self.user_manager.send_reset_email(self.trans, {"email": user_email}) + message = self.user_manager.send_reset_email(cast("GalaxyWebTransaction", self.trans), {"email": user_email}) assert message is None def test_get_user_by_identity(self): diff --git a/test/unit/app/tools/test_actions.py b/test/unit/app/tools/test_actions.py index 89d6a7dfa71..2f682063bd3 100644 --- a/test/unit/app/tools/test_actions.py +++ b/test/unit/app/tools/test_actions.py @@ -6,6 +6,7 @@ from typing import ( from galaxy import model from galaxy.app_unittest_utils import tools_support from galaxy.exceptions import UserActivationRequiredException +from galaxy.managers.context import ProvidesHistoryContext from galaxy.objectstore import BaseObjectStore from galaxy.tool_util.parser.output_objects import ToolOutput from galaxy.tool_util.parser.xml import parse_change_format @@ -191,7 +192,7 @@ class TestDefaultToolAction(TestCase, tools_support.UsesTools): self._init_tool(contents) job, out_data, *_ = self.action.execute( tool=self.tool, - trans=self.trans, + trans=cast(ProvidesHistoryContext, self.trans), history=self.history, incoming=incoming, ) diff --git a/test/unit/app/tools/test_select_parameters.py b/test/unit/app/tools/test_select_parameters.py index b24ba677211..9b0640aaa98 100644 --- a/test/unit/app/tools/test_select_parameters.py +++ b/test/unit/app/tools/test_select_parameters.py @@ -39,6 +39,13 @@ class TestSelectToolParameter(BaseParameterTestCase): self.trans.workflow_building_mode = True assert self.param.from_json("42", self.trans) == "42" + def test_get_initial_value_without_trans(self): + # conditional case inference calls this with no transaction + param = self._parameter_for( + xml="""""" + ) + assert param.get_initial_value(None, {}) == "b" + def test_validated_datasets(self): self.options_xml = """""" with pytest.raises(ValueError) as exc_info: diff --git a/test/unit/files/test_drs.py b/test/unit/files/test_drs.py index fe9676974df..62570bb38e9 100644 --- a/test/unit/files/test_drs.py +++ b/test/unit/files/test_drs.py @@ -2,7 +2,10 @@ import io import json import os import urllib -from typing import Any +from typing import ( + Any, + cast, +) from unittest import mock import pytest @@ -10,6 +13,7 @@ import responses from galaxy.files import ( DictFileSourcesUserContext, + ProvidesFileSourcesTransaction, ProvidesFileSourcesUserContext, ) from ._util import ( @@ -42,7 +46,7 @@ def test_provides_file_sources_user_context_oidc_access_tokens(): class DummyTrans: user = DummyUser() - tokens = ProvidesFileSourcesUserContext(DummyTrans()).oidc_access_tokens + tokens = ProvidesFileSourcesUserContext(cast(ProvidesFileSourcesTransaction, DummyTrans())).oidc_access_tokens assert tokens == {"oidc": "oidc-token", "keycloak": "keycloak-token"} @@ -52,7 +56,7 @@ def test_provides_file_sources_user_context_oidc_access_tokens_anonymous(): class DummyTrans: user = None - assert ProvidesFileSourcesUserContext(DummyTrans()).oidc_access_tokens is None + assert ProvidesFileSourcesUserContext(cast(ProvidesFileSourcesTransaction, DummyTrans())).oidc_access_tokens is None def test_drs_http_headers_template_expansion(): diff --git a/test/unit/webapps/test_login.py b/test/unit/webapps/test_login.py index ce5e2cb2a94..4aa1bb224e6 100644 --- a/test/unit/webapps/test_login.py +++ b/test/unit/webapps/test_login.py @@ -3,9 +3,11 @@ from datetime import ( datetime, timedelta, ) +from typing import cast from galaxy import model from galaxy.app_unittest_utils import galaxy_mock +from galaxy.managers.context import ProvidesAppContext from galaxy.managers.users import UserManager from galaxy.security.passwords import check_password from galaxy.util.unittest import TestCase @@ -87,7 +89,7 @@ class TestLoginController(TestCase): def test_get_reset_token(self): def _check_reset_token(email): - reset_user, prt = self.user_manager.get_reset_token(self.trans, email) + reset_user, prt = self.user_manager.get_reset_token(cast(ProvidesAppContext, self.trans), email) assert user2 == reset_user assert prt.user == user2 diff --git a/test/unit/webapps/test_send_file.py b/test/unit/webapps/test_send_file.py index 7a38315802a..e8b47112a5c 100644 --- a/test/unit/webapps/test_send_file.py +++ b/test/unit/webapps/test_send_file.py @@ -1,4 +1,5 @@ import tempfile +from typing import cast import pytest from a2wsgi import WSGIMiddleware @@ -11,6 +12,7 @@ from galaxy.web.framework.base import ( Response, send_file, ) +from galaxy.webapps.base.webapp import GalaxyWebTransaction CONTENT = "content" @@ -27,7 +29,7 @@ def setup_fastAPI(fh, nginx_x_accel_redirect_base=None, apache_xsendfile=None, s if set_content_length: trans.response.headers["content-length"] = len(CONTENT) trans.response.set_content_type("application/octet-stream") - return send_file(start_response, trans, fh) + return send_file(start_response, cast(GalaxyWebTransaction, trans), fh) app = FastAPI() # https://github.com/abersheeran/a2wsgi/issues/44 diff --git a/test/unit/workflows/test_workflow_markdown.py b/test/unit/workflows/test_workflow_markdown.py index 584955f33af..2e7adfe7420 100644 --- a/test/unit/workflows/test_workflow_markdown.py +++ b/test/unit/workflows/test_workflow_markdown.py @@ -1,6 +1,8 @@ +from typing import cast from unittest import mock from galaxy import model +from galaxy.managers.context import ProvidesHistoryContext from galaxy.managers.markdown_parse import validate_galaxy_markdown from galaxy.managers.markdown_util import ( populate_invocation_markdown, @@ -158,7 +160,9 @@ def populate_markdown(workflow_markdown): # Add invocation ids to internal Galaxy markdown trans = MockTrans() validate_galaxy_markdown(workflow_markdown) - galaxy_markdown = populate_invocation_markdown(trans, example_invocation(trans), workflow_markdown) + galaxy_markdown = populate_invocation_markdown( + cast(ProvidesHistoryContext, trans), example_invocation(trans), workflow_markdown + ) return galaxy_markdown @@ -170,7 +174,7 @@ def resolve_markdown(workflow_markdown): trans.app.workflow_manager = mock.MagicMock() invocation = example_invocation(trans) trans.app.workflow_manager.get_invocation.side_effect = [invocation, invocation] - galaxy_markdown = resolve_invocation_markdown(trans, workflow_markdown) + galaxy_markdown = resolve_invocation_markdown(cast(ProvidesHistoryContext, trans), workflow_markdown) return galaxy_markdown